diff --git a/.github/skills/api-review/SKILL.md b/.github/skills/api-review/SKILL.md new file mode 100644 index 00000000000..3c14ae9fc53 --- /dev/null +++ b/.github/skills/api-review/SKILL.md @@ -0,0 +1,504 @@ +--- +name: api-review +description: Reviews .NET API surface area PRs for design guideline violations. Analyzes api/*.cs file diffs, applies review rules from .NET Framework Design Guidelines and Aspire conventions, and attributes findings to the developer who introduced each API (via git blame). Use this when asked to review API surface area changes. +--- + +You are a .NET API review specialist for the dotnet/aspire repository. Your goal is to review API surface area PRs — the auto-generated `api/*.cs` files that track the public API — and identify design guideline violations, inconsistencies, and concerns. + +## Background + +Aspire uses auto-generated API files at `src/*/api/*.cs` and `src/Components/*/api/*.cs` to track the public API surface. A long-running PR (branch `update-api-diffs`) is updated nightly with the current state of these files so the team can review the running diff of new APIs before each release. This skill reviews those PRs. + +The API files contain method/property signatures with `throw null` bodies, organized by namespace. Example: + +```csharp +namespace Aspire.Hosting +{ + public static partial class ResourceBuilderExtensions + { + public static IResourceBuilder WithEndpoint(this IResourceBuilder builder, int? port = null) where T : IResourceWithEndpoints { throw null; } + } +} +``` + +## Task Execution Steps + +### Step 1: Get the PR Diff + +Get the diff of API files from the PR. The user will provide a PR number or URL. + +```bash +GH_PAGER=cat gh pr diff --repo dotnet/aspire -- 'src/*/api/*.cs' 'src/Components/*/api/*.cs' > /tmp/api-diff.txt +``` + +If that doesn't work (the `--` path filter is not always supported), get the full diff and filter: + +```bash +GH_PAGER=cat gh pr diff --repo dotnet/aspire > /tmp/full-diff.txt +``` + +Then extract only the API file sections manually by looking for `diff --git a/src/*/api/*.cs` headers. + +### Step 2: Parse the Diff + +From the diff, identify: + +1. **New files** — entirely new API surface files (new packages) +2. **Added lines** — lines starting with `+` (not `+++`) represent new or changed APIs +3. **Removed lines** — lines starting with `-` (not `---`) represent removed APIs +4. **Changed files** — which `api/*.cs` files were modified + +Group the changes by assembly/package (the file path tells you: `src/Aspire.Hosting.Redis/api/Aspire.Hosting.Redis.cs` → package `Aspire.Hosting.Redis`). + +### Step 3: Apply Review Rules + +For each new or changed API, apply the following rules. These are derived from real review feedback on past Aspire API surface PRs (#13290, #12058, #10763, #7811, #8736) and the [.NET Framework Design Guidelines](https://learn.microsoft.com/dotnet/standard/design-guidelines/). + +--- + +#### Rule 1: Visibility & Surface Area Minimization + +**Question whether new public types and members need to be public.** + +Look for: +- New `public class` or `public interface` declarations that look like implementation details (e.g., annotations, internal helpers, DCP model types) +- Types whose names suggest internal usage (containing words like `Internal`, `Helper`, `Impl`, `Handler`) +- Types in packages marked with `SuppressFinalPackageVersion` (preview packages) get a lighter touch — note it but don't flag as error +- Public methods with no obvious external use case + +*Past example: "Why is this class public?" / "Does this need to be public? I only see 1 caller and that's in the same assembly." / "These are more like implementation detail and the main thing we needed was the publishing hooks"* + +Severity: **Warning** + +--- + +#### Rule 2: Experimental Attribute + +**New unstable APIs should be marked `[Experimental]`.** + +Look for: +- New public types or members that are only used by other Experimental APIs but lack `[Experimental]` themselves +- New APIs in emerging feature areas (publishing, deployment, pipelines, compute) without `[Experimental]` +- Reuse of existing experimental diagnostic IDs — each `[Experimental("ASPIREXXXX")]` ID must be unique. Check that new experimental attributes don't reuse IDs already in the codebase + +*Past example: "Should this be Experimental?" / "Does the InputsDialogValidationContext need an Experimental attribute on it? The only place it is used is Experimental." / "These shouldn't reuse ASPIRECOMPUTE001 — that was already taken"* + +Severity: **Warning** + +--- + +#### Rule 3: Naming Conventions + +**Names must follow .NET naming guidelines and Aspire conventions.** + +Check for: +- **Method names must be verbs or verb phrases**: `GetSecret`, `CreateBuilder`, `AddResource` — not nouns. Flag methods like `ExecutionConfigurationBuilder()` that should be `CreateExecutionConfigurationBuilder()` +- **Type names must be nouns or noun phrases**: Classes, structs, interfaces should not start with verbs. Flag names like `CreateContainerFilesAnnotation` → should be `ContainerFilesCallbackAnnotation` +- **Consistent naming across similar APIs**: The codebase has established patterns. Flag inconsistencies: + - Property for URI-type values: should be `UriExpression` (not `ServiceUri`, `Endpoint`, or `Uri` inconsistently) + - `IServiceProvider`-typed properties: should be named `Services` (not `ServiceProvider`) — 25 uses of `Services` vs 10 of `ServiceProvider` in the codebase + - Port-related methods: `WithHostPort` or `WithGatewayPort` (not `WithHttpPort` — only one instance exists) +- **Interface names must match implementing class patterns**: If the class is `AzureKeyVaultResource`, the interface should be `IAzureKeyVaultResource` (not `IKeyVaultResource`) +- **Avoid confusingly similar names**: Flag pairs like `PipelineStepExtensions` / `PipelineStepsExtensions` (differ only by an 's') +- **Extension class placement**: Static factory methods like `Create*` should be on the type itself (e.g., `ParameterResource.Create()`), not in unrelated extension classes +- **PascalCase** for all public members +- **`I`-prefix** for all interfaces + +*Past examples: "The naming here gives me Java feels" / "This method name isn't a verb" / "UriExpression to be consistent" / "Why is this ContainerRegistryInfo and not ContainerRegistry?" / "We call this WithHostPort everywhere else"* + +Severity: **Warning** (naming inconsistency), **Error** (violates .NET guidelines) + +--- + +#### Rule 4: Namespace Placement + +**Types must be in appropriate, established namespaces.** + +Check for: +- Types in the global namespace (no `namespace` declaration) — always an error +- Public types in namespaces containing `Internal`, `Dcp`, or `Impl` — these are internal implementation namespaces +- Types in a different namespace than related types in the same assembly (e.g., a resource class in `Aspire.Hosting.Azure.AppService` when all others are in `Aspire.Hosting.Azure`) +- New namespaces being introduced without clear justification — prefer using existing namespaces +- Types in `Aspire.Hosting.Utils` or other utility namespaces — prefer `Aspire.Hosting.ApplicationModel` or the assembly's primary namespace + +*Past examples: "This should be in the same namespace as the rest of the resource classes" / "'Internal' namespace and public?" / "looks like this is the only type in Aspire.Hosting.Utils — is there a better namespace?" / "Missing a namespace on this type"* + +Severity: **Error** (no namespace / Internal namespace), **Warning** (inconsistent namespace) + +--- + +#### Rule 5: Parameter Design + +**Methods should have clean, versionable parameter lists.** + +Check for: +- **Too many optional parameters** (>5): These are extremely hard to version — suggest introducing an options/settings class. Flag the specific method and parameter count. + *Past example: "These APIs have way too many parameters. It might be cleaner to encapsulate some of these parameters into a specific options object" / "optional parameters at this scale are extremely hard to version — WithEndpoint is in the exact same jail"* +- **Redundant nullable default**: If a method has `string? publisherName = null` but a parameterless overload already exists, the `= null` is unnecessary and creates ambiguity +- **Inconsistent nullability**: Similar methods across the codebase should have consistent nullability. For example, if `WithHostPort(int? port)` is used everywhere else, a new `WithHostPort(int port)` is inconsistent +- **Boolean parameters**: Prefer enums over `bool` parameters for better readability and future extensibility + +Severity: **Warning** (too many params, bool params), **Error** (inconsistent nullability across same-named methods) + +--- + +#### Rule 6: Type Design + +**Types must follow .NET type design guidelines.** + +Check for: +- **`record struct` in public API**: Adding fields later is a binary breaking change. Flag and suggest using `record class` or `class` instead if the type may evolve + *Past example: "Does this need to be a record struct?" / discussion about binary breaking changes from adding fields* +- **`Tuple<>` in public API**: Never use tuples in public return types or parameters. Use dedicated types. + *Past example: "Using a Tuple in public API isn't the best — can the exception go on the interface?"* +- **Public fields**: Should be properties instead. Exception: `const` fields are fine. +- **`static readonly` that could be `const`**: String and primitive `static readonly` fields should typically be `const` + *Past example: "Why do we sometimes use static readonly and sometimes const?" — resolved by making all const* +- **Enum with `None` not at 0**: If an enum has a `None` or default member, it should be value 0 + *Past example: "Having None not be 0 is sort of odd"* +- **Missing sealed**: New public classes should be `sealed` unless extensibility is explicitly intended + +Severity: **Warning** + +--- + +#### Rule 7: Breaking Changes + +**Detect potentially breaking API changes.** + +Check for: +- Parameter nullability changes (nullable → non-nullable or vice versa) +- Removed public members from types or interfaces +- Changed base class or added new base class (may be binary breaking) +- Changed return types + +Severity: **Error** + +--- + +#### Rule 8: Consistency with Established Patterns + +**New APIs should follow patterns established elsewhere in the codebase.** + +Check for: +- **Resource types**: Should they implement `IResourceWithParent` if they have a parent relationship? +- **Emulator resources**: Should follow the same property/method patterns as existing emulators +- **Builder extension methods**: Must return `IResourceBuilder` for fluent chaining (or `IDistributedApplicationBuilder` for non-resource methods) +- **Connection properties**: Resources implementing `IResourceWithConnectionString` should expose consistent properties +- **AddPublisher/AddResource pattern**: `Add*` methods on `IDistributedApplicationBuilder` should return the builder for chaining + +*Past example: "AddPublisher should return IDistributedApplicationBuilder" / "Other emulator resources don't have this property" / "Other Resources don't have an IServiceProvider"* + +Severity: **Warning** + +--- + +#### Rule 9: Preview Package Awareness + +**New packages should ship as preview initially.** + +Check for: +- Entirely new API files (new assemblies) — note whether the package is likely preview or stable +- Brand new packages shipping as stable without sufficient bake time + +*Past example: "This new package is set to ship as stable for 9.2. Is that intentional?" / "I think preview" / "all brand new packages/integrations we are adding this release are set to stay in preview"* + +Severity: **Info** + +--- + +#### Rule 10: Obsolete API Hygiene + +**Obsolete APIs in preview packages are unnecessary overhead.** + +Check for: +- `[Obsolete]` attributes on APIs in packages that haven't shipped stable yet — these can just be renamed/removed directly +- `[EditorBrowsable(EditorBrowsableState.Never)]` combined with `[Obsolete]` in preview packages + +*Past example: "This library is in preview mode. Do we need Obsolete/EBS.Never properties? Can we just change the names in the major version?"* + +Severity: **Info** + +--- + +### Step 4: Git Attribution (REQUIRED before posting) + +**Every finding MUST include author attribution before posting to the PR.** This is critical — it routes feedback to the right person and ensures accountability. + +For each finding, identify who introduced the API change using `git blame` on the **source file** (not the auto-generated `api/*.cs` file). + +#### 4a: Find the source file + +The API files at `src/*/api/*.cs` are auto-generated. To find the actual source, search for the class/method name in the non-API source files: + +```bash +# Find the source file for a given API (e.g., WithRemoteImageName) +git grep -rn "WithRemoteImageName" -- "src/**/*.cs" ":!src/*/api/*.cs" | head -5 +``` + +#### 4b: Blame the source file + +Once you have the source file and line number, use `git blame` to find the author: + +```bash +# Get the author of a specific line +git blame -L , --porcelain | grep -E "^author |^author-mail " +``` + +Run blame in batch for efficiency — collect all source file locations first, then blame them all at once. + +#### 4c: Map email to GitHub username + +Common Aspire contributors and their GitHub usernames: +- `mitch@mitchdeny.com` → `@mitchdenny` +- `eric.erhardt@microsoft.com` → `@eerhardt` +- `davidfowl@gmail.com` → `@davidfowl` +- `james@newtonking.com` → `@JamesNK` +- `sebastienros@gmail.com` → `@sebastienros` + +For unknown emails, look up the commit on GitHub: + +```bash +GH_PAGER=cat gh api "/repos/dotnet/aspire/commits/" --jq '.author.login // .commit.author.name' +``` + +#### 4d: Format attribution + +Prepend each review comment body with `cc @username` on its own line, followed by a blank line before the finding text. Example: + +``` +cc @username + +❌ **[Breaking Change]** Description of the issue... +``` + +**Do not skip this step.** If you cannot determine the author, use the git log pickaxe search as a fallback: + +```bash +git log main -S "" --pretty=format:"%H|%an|%ae|%s" -- "src/**/*.cs" ":!src/*/api/*.cs" | head -5 +``` + +### Step 5: Generate Review Report + +Present findings in a structured format, grouped by severity: + +```markdown +## API Review Findings + +### ❌ Errors (must fix before release) + +| # | File | API | Rule | Issue | Author | +|---|------|-----|------|-------|--------| +| 1 | Aspire.Hosting.cs | `SomeType` | Namespace | Type in global namespace | @user (abc1234) | + +### ⚠️ Warnings (should address) + +| # | File | API | Rule | Issue | Author | +|---|------|-----|------|-------|--------| +| 1 | Aspire.Hosting.cs | `WithFoo(...)` | Parameters | 8 optional params — consider options object | @user (def5678) | + +### ℹ️ Info (consider) + +| # | File | API | Rule | Issue | Author | +|---|------|-----|------|-------|--------| +| 1 | Aspire.Hosting.NewPkg.cs | (entire file) | Preview | New package — verify shipping as preview | @user (ghi9012) | + +### Summary +- **X errors**, **Y warnings**, **Z info** across N files +- Top areas of concern: [list] +``` + +### Step 6: Post Review Comments on the PR + +After generating the report, **post each finding as a separate PR review comment** so the team can discuss each one independently. + +#### Check for existing reviews (deduplication) + +Before posting, check whether you have already posted a review on this PR. If so, **update or skip rather than duplicate**. + +```python +# check_existing_reviews.py — detect prior API review comments +import subprocess, json, os + +env = {**os.environ, 'GH_PAGER': 'cat'} + +# Get the current authenticated user +result = subprocess.run(['gh', 'api', '/user', '--jq', '.login'], + capture_output=True, text=True, env=env) +current_user = result.stdout.strip() + +# Fetch all review comments on the PR by the current user +result = subprocess.run([ + 'gh', 'api', '--paginate', + '/repos/dotnet/aspire/pulls//comments' +], capture_output=True, text=True, encoding='utf-8', env=env) +all_comments = json.loads(result.stdout) + +my_comments = [c for c in all_comments if c['user']['login'] == current_user] +api_review_comments = [c for c in my_comments + if any(marker in c['body'] for marker in + ['[Breaking Change]', '[Parameter Design]', '[Namespace', + '[Visibility]', '[Type Design]', '[Preview Package]', + '[Obsolete API', '[Naming', '[Experimental', '[Consistency'])] + +if api_review_comments: + print(f"Found {len(api_review_comments)} existing API review comments by @{current_user}") + for c in api_review_comments: + print(f" - {c['id']}: {c['path']}:{c.get('line', '?')} | {c['body'][:60]}...") +else: + print("No existing API review comments found — safe to post.") +``` + +**If existing comments are found:** +1. Compare each new finding against existing comments (match by file path + rule name) +2. **Skip** findings that already have a matching comment +3. **Update** existing comments (via `PATCH /repos/{owner}/{repo}/pulls/comments/{comment_id}`) if the finding text has changed +4. **Post only net-new** findings that don't have a matching existing comment + +**If no existing comments are found**, proceed to post all findings. + +#### Determine line numbers for inline comments + +For each finding, determine the line number in the diff where the API appears. Parse the diff hunks from Step 1 to map API declarations to their line numbers in the changed files. + +```bash +# Get the diff with line numbers to map findings to positions +GH_PAGER=cat gh pr diff --repo dotnet/aspire > /tmp/full-diff.txt +``` + +For each added API line (starting with `+`), count its position within the diff hunk to determine the diff-relative line number. + +#### Post each finding as a separate inline review comment + +Each violation gets its own comment so it can be discussed independently. **Every comment MUST include the `cc @username` attribution from Step 4.** Do not post comments without attribution. + +Use the GitHub API to post individual review comments on the specific lines. Build the JSON payload using Python to avoid encoding issues (see Constraint #9), then post with `gh api --input`: + +```python +# build_review.py — construct the review JSON and post it +import json, subprocess, os + +pr_head_sha = "" # from: gh pr view --json headRefOid --jq '.headRefOid' + +review = { + "commit_id": pr_head_sha, + "event": "COMMENT", + "body": "API review findings — see inline comments for details.", + "comments": [ + { + "path": "src/Aspire.Hosting/api/Aspire.Hosting.cs", + "line": 42, + "side": "RIGHT", + "body": "cc @username\n\n⚠️ **[Parameter Design]** `AllowInbound` has 6 optional parameters — consider introducing an options class.\n\nRef: [Parameter Design Guidelines](https://learn.microsoft.com/dotnet/standard/design-guidelines/parameter-design)" + }, + { + "path": "src/Aspire.Hosting.Azure.Network/api/Aspire.Hosting.Azure.Network.cs", + "line": 15, + "side": "RIGHT", + "body": "cc @username\n\nℹ️ **[Preview Package]** Entirely new package — verify it is shipping as preview (SuppressFinalPackageVersion=true)." + } + ] +} + +tmpfile = os.path.join(os.environ.get('TEMP', '/tmp'), 'review.json') +with open(tmpfile, 'w', encoding='utf-8') as f: + json.dump(review, f, ensure_ascii=False) + +subprocess.run([ + 'gh', 'api', '--method', 'POST', + '/repos/dotnet/aspire/pulls//reviews', + '--input', tmpfile +], env={**os.environ, 'GH_PAGER': 'cat'}) +``` + +Each comment in the `comments` array becomes a **separate review thread** on the PR, allowing independent discussion. + +**Comment format for each finding:** + +``` +cc @username + +[severity emoji] **[Rule Name]** Description of the issue. + +Ref: [Guideline link](url) +``` + +The `cc @username` line MUST be the first line of every comment. This tags the original author of the API so they get notified. + +Severity emojis: ❌ Error, ⚠️ Warning, ℹ️ Info + +**Note on `side` field**: Always include `"side": "RIGHT"` for comments on added lines (the new file version). + +#### Fallback: file-level comments + +If a finding applies to an entire new file (e.g., "new package — verify preview status") or the exact line cannot be determined, post an inline comment on line 1 of the file: + +```bash +{ + "path": "src/Aspire.Hosting.NewPkg/api/Aspire.Hosting.NewPkg.cs", + "line": 1, + "side": "RIGHT", + "body": "cc @username\n\nℹ️ **[Preview Package]** ..." +} +``` + +**Note**: Do NOT use `"subject_type": "file"` — the GitHub PR reviews API does not support this field. Always use `"line": 1` with `"side": "RIGHT"` instead. + +#### Post a summary comment at the end + +After all inline comments are posted, post a single top-level summary comment on the PR: + +```bash +GH_PAGER=cat gh pr comment --repo dotnet/aspire --body "## API Review Summary + +**X errors**, **Y warnings**, **Z info** across N files. + +Top areas of concern: +- [list key themes] + +Each finding is posted as a separate inline comment for discussion." +``` + +#### Ask before posting + +Before posting, show the user the list of comments that will be posted (including author attributions) and ask for confirmation. Do not post without approval. + +## Important Constraints + +1. **Only review `api/*.cs` files** — these are the source of truth for public API surface +2. **Focus on added/changed lines** — don't review existing APIs that haven't changed +3. **Check both sides of a rename** — if an API was removed and a similar one added, it's likely a rename, not a removal + addition +4. **Be pragmatic** — APIs in preview packages (`SuppressFinalPackageVersion`) get lighter scrutiny +5. **Don't flag auto-generated formatting** — the API tool controls formatting; only review API design +6. **Cross-reference related files** — when questioning visibility, check if the type is used in other Aspire assemblies (which would require it to be public) +7. **Attribute to the right person** — search source code history, not API file history (API files are auto-generated). Use `git blame` on the actual source `.cs` files. Every comment MUST tag the original author with `cc @username` as the first line. +8. **Include `side: "RIGHT"` in all review comments** — the GitHub API requires this for inline comments on added lines +9. **Use Python (not PowerShell) for building JSON payloads** — PowerShell's `ConvertTo-Json | Out-File` mangles multi-byte Unicode characters (emojis like ❌ ⚠️ ℹ️ and em-dashes — become mojibake). Always use Python's `json.dumps(ensure_ascii=False)` with `open(file, 'w', encoding='utf-8')` when constructing JSON for the GitHub API. + +## Reference: .NET Framework Design Guidelines + +The rules above are grounded in Microsoft's official guidelines at: +- [Framework Design Guidelines](https://learn.microsoft.com/dotnet/standard/design-guidelines/) +- [Naming Guidelines](https://learn.microsoft.com/dotnet/standard/design-guidelines/naming-guidelines) +- [Type Design Guidelines](https://learn.microsoft.com/dotnet/standard/design-guidelines/type) +- [Member Design Guidelines](https://learn.microsoft.com/dotnet/standard/design-guidelines/member) +- [Parameter Design](https://learn.microsoft.com/dotnet/standard/design-guidelines/parameter-design) +- [Designing for Extensibility](https://learn.microsoft.com/dotnet/standard/design-guidelines/designing-for-extensibility) +- [.NET Breaking Change Rules](https://learn.microsoft.com/dotnet/core/compatibility/library-change-rules) + +## Reference: Aspire-Specific Conventions + +These conventions are specific to the Aspire codebase, learned from past API reviews: + +| Convention | Example | Notes | +|-----------|---------|-------| +| URI properties named `UriExpression` | `public ReferenceExpression UriExpression` | Not `ServiceUri`, `Endpoint`, `Uri` | +| IServiceProvider property named `Services` | `public IServiceProvider Services` | Not `ServiceProvider` (25 vs 10 usage) | +| Port methods accept nullable | `WithHostPort(int? port)` | Allows random port assignment | +| Enums: None/default = 0 | `None = 0, Append = 1` | Not `Append = 0, None = 1` | +| Builder methods return builder | `IResourceBuilder` return | For fluent chaining | +| Extension static factories → type statics | `ParameterResource.Create()` | Not `ParameterResourceBuilderExtensions.Create()` | +| New packages ship preview | `true` | Until sufficient bake time | +| No Obsolete in preview packages | Just rename/remove directly | Don't add migration overhead | +| Resource types use established namespaces | `Aspire.Hosting.Azure` | Not sub-namespaces per resource | +| `const` over `static readonly` for strings | `public const string Tag = "8.6"` | Unless runtime computation needed | diff --git a/.github/workflows/polyglot-validation/test-typescript-playground.sh b/.github/workflows/polyglot-validation/test-typescript-playground.sh index cd214826588..d5125f3fb22 100644 --- a/.github/workflows/polyglot-validation/test-typescript-playground.sh +++ b/.github/workflows/polyglot-validation/test-typescript-playground.sh @@ -55,9 +55,45 @@ echo "" FAILED=() PASSED=() +SKIPPED=() + +# Packages that only produce prerelease versions (SuppressFinalPackageVersion=true) cannot be +# restored when the build is stabilized, because aspire restore requests stable versions. +# Skip these until the build infrastructure dynamically computes versions. See #15335. +SKIP_PREVIEW_ONLY=( + "Aspire.Hosting.Azure.Kusto" + "Aspire.Hosting.Azure.Network" + "Aspire.Hosting.Azure.Sql" + "Aspire.Hosting.Docker" + "Aspire.Hosting.Foundry" + "Aspire.Hosting.Keycloak" + "Aspire.Hosting.Kubernetes" + "Aspire.Hosting.Maui" +) for app_dir in "${APP_DIRS[@]}"; do app_name="$(basename "$(dirname "$app_dir")")/$(basename "$app_dir")" + integration_name="$(basename "$(dirname "$app_dir")")" + + # Check if this integration is in the skip list + skip=false + for skip_pkg in "${SKIP_PREVIEW_ONLY[@]}"; do + if [ "$integration_name" = "$skip_pkg" ]; then + skip=true + break + fi + done + + if [ "$skip" = true ]; then + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo "Testing: $app_name" + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + echo " ⏭ Skipping (preview-only package, see #15335)" + SKIPPED+=("$app_name") + echo "" + continue + fi + echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "Testing: $app_name" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" @@ -97,7 +133,7 @@ done echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo "Results: ${#PASSED[@]} passed, ${#FAILED[@]} failed out of ${#APP_DIRS[@]} apps" +echo "Results: ${#PASSED[@]} passed, ${#FAILED[@]} failed, ${#SKIPPED[@]} skipped out of ${#APP_DIRS[@]} apps" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" if [ ${#FAILED[@]} -gt 0 ]; then diff --git a/.github/workflows/refresh-typescript-sdks.yml b/.github/workflows/refresh-typescript-sdks.yml new file mode 100644 index 00000000000..a465371d17b --- /dev/null +++ b/.github/workflows/refresh-typescript-sdks.yml @@ -0,0 +1,45 @@ +name: Refresh TypeScript Playground SDKs + +on: + workflow_dispatch: + schedule: + - cron: '0 16 * * *' # 8am PT / 16:00 UTC - same schedule as manifest refresh + +permissions: + contents: write + pull-requests: write + +jobs: + generate-and-pr: + runs-on: ubuntu-latest + if: ${{ github.repository_owner == 'microsoft' }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + + - name: Setup .NET SDK + uses: actions/setup-dotnet@67a3573c9a986a3f9c594539f4ab511d57bb3ce9 # v4 + with: + global-json-file: global.json + + - name: Setup Node.js + uses: actions/setup-node@1d0ff469b7ec7b3cb9d8673fde0c81c44821de2a # v4.2.0 + with: + node-version: 20.x + + - name: Run refreshTypeScriptSdks script + shell: pwsh + run: | + ./eng/refreshTypeScriptSdks.ps1 + + - name: Create or update pull request + uses: dotnet/actions-create-pull-request@e8d799aa1f8b17f324f9513832811b0a62f1e0b1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branch: update-typescript-playground-sdks + base: main + commit-message: "[Automated] Update TypeScript Playground AppHost SDKs" + labels: | + area-app-model + area-engineering-systems + title: "[Automated] Update TypeScript Playground AppHost SDKs" + body: "Auto-generated update to refresh the TypeScript playground AppHost SDKs." diff --git a/AGENTS.md b/AGENTS.md index 98388f1c53d..3675b0d6dc1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -392,6 +392,7 @@ The following specialized skills are available in `.github/skills/`: - **test-management**: Quarantines or disables flaky/problematic tests using the QuarantineTools utility - **connection-properties**: Expert for creating and improving Connection Properties in Aspire resources - **dependency-update**: Guides dependency version updates by checking nuget.org, triggering the dotnet-migrate-package Azure DevOps pipeline, and monitoring runs +- **api-review**: Reviews .NET API surface area PRs for design guideline violations, applies rules from .NET Framework Design Guidelines and Aspire conventions, and attributes findings to the author who introduced each API - **startup-perf**: Measures Aspire application startup performance using dotnet-trace and the TraceAnalyzer tool ## Pattern-Based Instructions diff --git a/Directory.Packages.props b/Directory.Packages.props index d9902903798..0c4dc08b4b3 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -38,7 +38,7 @@ - + @@ -52,7 +52,7 @@ - + @@ -118,7 +118,7 @@ - + diff --git a/eng/Versions.props b/eng/Versions.props index 09150986ec2..4605855ca6e 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -41,7 +41,7 @@ 11.0.0-beta.25610.3 10.2.0 - 10.1.0-preview.1.25608.1 + 10.2.0-preview.1.26063.2 10.0.0-preview.1.25559.3 10.2.0 10.2.0 diff --git a/eng/homebrew/README.md b/eng/homebrew/README.md index 44b3ec4d7a0..0f5bf88c142 100644 --- a/eng/homebrew/README.md +++ b/eng/homebrew/README.md @@ -8,7 +8,6 @@ Aspire CLI is distributed via [Homebrew Cask](https://docs.brew.sh/Cask-Cookbook ```bash brew install --cask aspire # stable -# brew install --cask aspire@prerelease # preview (not yet supported) ``` ## Contents @@ -16,14 +15,13 @@ brew install --cask aspire # stable | File | Description | |---|---| | `aspire.rb.template` | Cask template for stable releases | -| `aspire@prerelease.rb.template` | Cask template for prerelease builds | | `generate-cask.sh` | Downloads tarballs, computes SHA256 hashes, generates cask from template | ### Pipeline templates | File | Description | |---|---| -| `eng/pipelines/templates/prepare-homebrew-cask.yml` | Generates, validates, audits, and tests the cask | +| `eng/pipelines/templates/prepare-homebrew-cask.yml` | Generates, styles, validates, audits, and tests the cask | | `eng/pipelines/templates/publish-homebrew.yml` | Submits the cask as a PR to `Homebrew/homebrew-cask` | ## Supported Platforms @@ -33,22 +31,22 @@ macOS only (arm64, x64). The cask uses `arch arm: "arm64", intel: "x64"` for URL ## Artifact URLs ```text -https://ci.dot.net/public/aspire/{VERSION}/aspire-cli-osx-{arch}-{VERSION}.tar.gz +https://ci.dot.net/public/aspire/{ARTIFACT_VERSION}/aspire-cli-osx-{arch}-{VERSION}.tar.gz ``` Where arch is `arm64` or `x64`. ## Why Cask -| Product | Type | Install command | Preview channel | -|---|---|---|---| -| GitHub Copilot CLI | homebrew-cask | `brew install --cask copilot-cli` | `copilot-cli@prerelease` | -| .NET SDK | homebrew-cask | `brew install --cask dotnet-sdk` | `dotnet-sdk@preview` | -| PowerShell | homebrew-cask | `brew install --cask powershell` | `powershell@preview` | +| Product | Type | Install command | +|---|---|---| +| GitHub Copilot CLI | homebrew-cask | `brew install --cask copilot-cli` | +| .NET SDK | homebrew-cask | `brew install --cask dotnet-sdk` | +| PowerShell | homebrew-cask | `brew install --cask powershell` | - **URL templating**: `url "...osx-#{arch}-#{version}.tar.gz"` — a single line instead of nested `on_macos do / if Hardware::CPU.arm?` blocks - **Official repo path**: Casks can be submitted to `Homebrew/homebrew-cask` for `brew install aspire` without a tap -- **Cleaner multi-channel**: `aspire` and `aspire@prerelease` follow established cask naming conventions +- **Stable-only release flow**: the current Aspire Homebrew publishing pipeline prepares and submits only the stable `aspire` cask, while a separate prerelease cask remains a possible future option ## CI Pipeline @@ -57,17 +55,26 @@ Where arch is `arm64` or `x64`. | `azure-pipelines.yml` (prepare stage) | Stable casks (artifacts only) | — | | `release-publish-nuget.yml` (release) | — | Stable cask only | -Publishing submits a PR to `Homebrew/homebrew-cask` using `gh pr create`: +Publishing submits a PR to `Homebrew/homebrew-cask` using the GitHub REST API: 1. Forks `Homebrew/homebrew-cask` (idempotent — reuses existing fork) -2. Creates a branch named `aspire-{version}` -3. Copies the generated cask to `Casks/a/aspire.rb` (or `aspire@prerelease.rb`) -4. Pushes and opens a PR with title `aspire {version}` +2. Creates or resets a branch named `aspire-{version}` +3. Copies the generated cask to `Casks/a/aspire.rb` +4. Reuses the existing open PR for that branch when present +5. Force-pushes the same branch for reruns; if prior PRs from that branch were closed, the publish step opens a fresh PR and marks the old ones as superseded +6. Opens a PR with title `aspire {version}` when none exists + +Prepare validation currently runs: + +1. `ruby -c` for syntax validation +2. `brew style --fix` on the generated cask +3. `brew audit --cask --online`, or `brew audit --cask --new --online` when the cask does not yet exist upstream +4. `HOMEBREW_NO_INSTALL_FROM_API=1 brew install --cask ...` followed by uninstall validation ## Open Items - [ ] Submit initial `aspire` cask PR to `Homebrew/homebrew-cask` for acceptance -- [ ] Submit `aspire@prerelease` cask PR to `Homebrew/homebrew-cask` +- [ ] (Future) Decide whether to add a separate prerelease cask (for example, `aspire@prerelease`) and update pipelines/docs accordingly - [ ] Configure `aspire-homebrew-bot-pat` secret in the pipeline variable group ## References diff --git a/eng/homebrew/aspire.rb.template b/eng/homebrew/aspire.rb.template index 5c749689faa..dbe07554fe3 100644 --- a/eng/homebrew/aspire.rb.template +++ b/eng/homebrew/aspire.rb.template @@ -2,17 +2,15 @@ cask "aspire" do arch arm: "arm64", intel: "x64" version "${VERSION}" - sha256 arm: "${SHA256_OSX_ARM64}", - intel: "${SHA256_OSX_X64}" + sha256 arm: "${SHA256_OSX_ARM64}", + intel: "${SHA256_OSX_X64}" - url "https://ci.dot.net/public/aspire/#{version}/aspire-cli-osx-#{arch}-#{version}.tar.gz", + url "https://ci.dot.net/public/aspire/${ARTIFACT_VERSION}/aspire-cli-osx-#{arch}-#{version}.tar.gz", verified: "ci.dot.net/public/aspire/" name "Aspire CLI" - desc "CLI tool for building observable, production-ready distributed applications with Aspire" + desc "CLI for building observable, production-ready distributed applications" homepage "https://aspire.dev/" - conflicts_with cask: "aspire@prerelease" - binary "aspire" zap trash: "~/.aspire" diff --git a/eng/homebrew/aspire@prerelease.rb.template b/eng/homebrew/aspire@prerelease.rb.template deleted file mode 100644 index ebb0f45a0ad..00000000000 --- a/eng/homebrew/aspire@prerelease.rb.template +++ /dev/null @@ -1,19 +0,0 @@ -cask "aspire@prerelease" do - arch arm: "arm64", intel: "x64" - - version "${VERSION}" - sha256 arm: "${SHA256_OSX_ARM64}", - intel: "${SHA256_OSX_X64}" - - url "https://ci.dot.net/public/aspire/#{version}/aspire-cli-osx-#{arch}-#{version}.tar.gz", - verified: "ci.dot.net/public/aspire/" - name "Aspire CLI (Prerelease)" - desc "CLI tool for building observable, production-ready distributed applications with Aspire" - homepage "https://aspire.dev/" - - conflicts_with cask: "aspire" - - binary "aspire" - - zap trash: "~/.aspire" -end diff --git a/eng/homebrew/dogfood.sh b/eng/homebrew/dogfood.sh index 8d0a44601a5..fbe7ac44689 100755 --- a/eng/homebrew/dogfood.sh +++ b/eng/homebrew/dogfood.sh @@ -33,17 +33,20 @@ EOF exit 0 } +is_cask_installed() { + local caskName="$1" + + brew list --cask --versions 2>/dev/null | awk '{print $1}' | grep -Fx -- "$caskName" >/dev/null +} + uninstall() { echo "Uninstalling dogfooded Aspire CLI..." - # Determine which cask is installed - for caskName in "aspire" "aspire@prerelease"; do - if brew list --cask "$TAP_NAME/$caskName" &>/dev/null; then - echo " Uninstalling $TAP_NAME/$caskName..." - brew uninstall --cask "$TAP_NAME/$caskName" - echo " Uninstalled." - fi - done + if brew list --cask "$TAP_NAME/aspire" &>/dev/null; then + echo " Uninstalling $TAP_NAME/aspire..." + brew uninstall --cask "$TAP_NAME/aspire" + echo " Uninstalled." + fi if brew tap-info "$TAP_NAME" &>/dev/null; then echo " Removing tap $TAP_NAME..." @@ -74,16 +77,14 @@ fi # Auto-detect cask file if not specified if [[ -z "$CASK_FILE" ]]; then - for candidate in "$SCRIPT_DIR/aspire.rb" "$SCRIPT_DIR/aspire@prerelease.rb"; do - if [[ -f "$candidate" ]]; then - CASK_FILE="$candidate" - break - fi - done + candidate="$SCRIPT_DIR/aspire.rb" + if [[ -f "$candidate" ]]; then + CASK_FILE="$candidate" + fi if [[ -z "$CASK_FILE" ]]; then echo "Error: No cask file found in $SCRIPT_DIR" - echo "Expected aspire.rb or aspire@prerelease.rb" + echo "Expected aspire.rb" exit 1 fi fi @@ -97,20 +98,24 @@ CASK_FILE="$(cd "$(dirname "$CASK_FILE")" && pwd)/$(basename "$CASK_FILE")" CASK_FILENAME="$(basename "$CASK_FILE")" CASK_NAME="${CASK_FILENAME%.rb}" +if [[ "$CASK_NAME" != "aspire" ]]; then + echo "Error: Only the stable Homebrew cask is supported." + echo "Expected aspire.rb" + exit 1 +fi + echo "Aspire CLI Homebrew Dogfood Installer" echo "======================================" echo " Cask file: $CASK_FILE" echo " Cask name: $CASK_NAME" echo "" -# Check for conflicts with official installs -for check in "aspire" "aspire@prerelease"; do - if brew list --cask "$check" &>/dev/null; then - echo "Error: '$check' is already installed from the official Homebrew tap." - echo "Uninstall it first with: brew uninstall --cask $check" - exit 1 - fi -done +if is_cask_installed "aspire"; then + echo "Error: 'aspire' is already installed." + echo "If this is a previous dogfood install, remove it with: $(basename "$0") --uninstall" + echo "Otherwise uninstall it first with: brew uninstall --cask aspire" + exit 1 +fi # Check for leftover local/aspire tap from pipeline testing if brew tap-info "local/aspire" &>/dev/null 2>&1; then @@ -128,12 +133,9 @@ fi # Clean up any previous dogfood tap if brew tap-info "$TAP_NAME" &>/dev/null 2>&1; then echo "Removing previous dogfood tap..." - # Uninstall any casks from the old tap first - for old in "aspire" "aspire@prerelease"; do - if brew list --cask "$TAP_NAME/$old" &>/dev/null; then - brew uninstall --cask "$TAP_NAME/$old" || true - fi - done + if is_cask_installed "aspire"; then + brew uninstall --cask "aspire" || true + fi brew untap "$TAP_NAME" fi diff --git a/eng/homebrew/generate-cask.sh b/eng/homebrew/generate-cask.sh index 906bed34da3..53727c781f3 100755 --- a/eng/homebrew/generate-cask.sh +++ b/eng/homebrew/generate-cask.sh @@ -7,14 +7,15 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" usage() { cat <&2 + echo " Path: $file_path" >&2 + + local hash + hash="$(shasum -a 256 "$file_path" | awk '{print $1}')" + echo " SHA256: $hash" >&2 + echo "$hash" +} + +find_local_archive() { + local archive_name="$1" + local archive_path + local matches=() + local match + + while IFS= read -r archive_path; do + matches+=("$archive_path") + done < <(find "$ARCHIVE_ROOT" -type f -name "$archive_name" -print | LC_ALL=C sort) + + if [[ "${#matches[@]}" -eq 0 ]]; then + echo "Error: Could not find local archive '$archive_name' under '$ARCHIVE_ROOT'" >&2 + exit 1 + fi + + if [[ "${#matches[@]}" -gt 1 ]]; then + echo "Error: Found multiple local archives named '$archive_name' under '$ARCHIVE_ROOT':" >&2 + for match in "${matches[@]}"; do + echo " $match" >&2 + done + exit 1 + fi + + echo "${matches[0]}" +} + # Check if a URL is accessible (HEAD request) url_exists() { curl -o /dev/null -s --head --fail "$1" } -echo "Generating Homebrew cask for Aspire version $VERSION (channel: $CHANNEL)" +echo "Generating Homebrew cask for Aspire version $VERSION" echo "" # macOS tarballs are required OSX_ARM64_URL="$BASE_URL/aspire-cli-osx-arm64-$VERSION.tar.gz" OSX_X64_URL="$BASE_URL/aspire-cli-osx-x64-$VERSION.tar.gz" +if [[ -n "$ARCHIVE_ROOT" && ! -d "$ARCHIVE_ROOT" ]]; then + echo "Error: --archive-root directory does not exist: $ARCHIVE_ROOT" + exit 1 +fi + # Validate URLs are accessible before downloading (fast-fail) if [[ "$VALIDATE_URLS" == true ]]; then echo "Validating tarball URLs..." @@ -115,8 +155,16 @@ if [[ "$VALIDATE_URLS" == true ]]; then echo "" fi -SHA256_OSX_ARM64="$(compute_sha256 "$OSX_ARM64_URL" "macOS ARM64 tarball")" -SHA256_OSX_X64="$(compute_sha256 "$OSX_X64_URL" "macOS x64 tarball")" +if [[ -n "$ARCHIVE_ROOT" ]]; then + OSX_ARM64_ARCHIVE="$(find_local_archive "aspire-cli-osx-arm64-$VERSION.tar.gz")" + OSX_X64_ARCHIVE="$(find_local_archive "aspire-cli-osx-x64-$VERSION.tar.gz")" + + SHA256_OSX_ARM64="$(compute_sha256_from_file "$OSX_ARM64_ARCHIVE" "macOS ARM64 tarball")" + SHA256_OSX_X64="$(compute_sha256_from_file "$OSX_X64_ARCHIVE" "macOS x64 tarball")" +else + SHA256_OSX_ARM64="$(compute_sha256 "$OSX_ARM64_URL" "macOS ARM64 tarball")" + SHA256_OSX_X64="$(compute_sha256 "$OSX_X64_URL" "macOS x64 tarball")" +fi echo "" echo "Generating cask from template..." @@ -124,6 +172,7 @@ echo "Generating cask from template..." # Read template and perform substitutions content="$(cat "$TEMPLATE")" content="${content//\$\{VERSION\}/$VERSION}" +content="${content//\$\{ARTIFACT_VERSION\}/$ARTIFACT_VERSION}" content="${content//\$\{SHA256_OSX_ARM64\}/$SHA256_OSX_ARM64}" content="${content//\$\{SHA256_OSX_X64\}/$SHA256_OSX_X64}" diff --git a/eng/pipelines/azure-pipelines-unofficial.yml b/eng/pipelines/azure-pipelines-unofficial.yml index e4147cdf65d..44a7668ba1e 100644 --- a/eng/pipelines/azure-pipelines-unofficial.yml +++ b/eng/pipelines/azure-pipelines-unofficial.yml @@ -145,6 +145,7 @@ extends: preSteps: - checkout: self fetchDepth: 1 + clean: true steps: - task: DownloadPipelineArtifact@2 @@ -208,3 +209,139 @@ extends: targetRids: - win-x64 - win-arm64 + - pwsh: | + $ErrorActionPreference = 'Stop' + $shippingDir = "$(Build.SourcesDirectory)/artifacts/packages/$(_BuildConfig)/Shipping" + $nupkg = Get-ChildItem -Path $shippingDir -Filter "Aspire.Hosting.AppHost.*.nupkg" -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notmatch '\.symbols\.' } | + Select-Object -First 1 + + if (-not $nupkg) { + Write-Error "Could not find Aspire.Hosting.AppHost nupkg in $shippingDir" + exit 1 + } + + $version = $nupkg.Name -replace '^Aspire\.Hosting\.AppHost\.', '' -replace '\.nupkg$', '' + + if ($version -notmatch '^\d+\.\d+\.\d+') { + Write-Error "Extracted version '$version' does not look like a valid semver. Check the nupkg name." + exit 1 + } + + Write-Host "Detected Aspire version: $version (from $($nupkg.Name))" + Write-Host "##vso[task.setvariable variable=aspireVersion;isOutput=true]$version" + name: computeVersion + displayName: 🟣Compute Aspire version from nupkg + + - pwsh: | + $ErrorActionPreference = 'Stop' + $artifactVersion = dotnet msbuild "$(Build.SourcesDirectory)/src/Aspire.Dashboard/Aspire.Dashboard.csproj" -getProperty:PackageVersion -property:SuppressFinalPackageVersion=true -property:OfficialBuildId=$(BUILD.BUILDNUMBER) + $artifactVersion = $artifactVersion.Trim() + + if ($artifactVersion -notmatch '^\d+\.\d+\.\d+') { + Write-Error "Computed artifact version '$artifactVersion' does not look like a valid semver. Check PackageVersion evaluation." + exit 1 + } + + Write-Host "Detected installer artifact version: $artifactVersion" + Write-Host "##vso[task.setvariable variable=aspireArtifactVersion;isOutput=true]$artifactVersion" + name: computeArtifactVersion + displayName: 🟣Compute installer artifact version + + - pwsh: | + $ErrorActionPreference = 'Stop' + $versionKind = dotnet msbuild "$(Build.SourcesDirectory)/eng/Versions.props" -getProperty:DotNetFinalVersionKind + $versionKind = $versionKind.Trim() + Write-Host "DotNetFinalVersionKind: '$versionKind'" + + if ($versionKind -eq 'release') { + $channel = 'stable' + } else { + $channel = 'prerelease' + } + + Write-Host "Installer channel: $channel" + Write-Host "##vso[task.setvariable variable=installerChannel;isOutput=true]$channel" + name: computeChannel + displayName: 🟣Determine installer channel + + - stage: prepare_installers + displayName: Prepare Installers + dependsOn: + - build + variables: + aspireVersion: $[ stageDependencies.build.Windows.outputs['computeVersion.aspireVersion'] ] + aspireArtifactVersion: $[ stageDependencies.build.Windows.outputs['computeArtifactVersion.aspireArtifactVersion'] ] + installerChannel: $[ stageDependencies.build.Windows.outputs['computeChannel.installerChannel'] ] + condition: | + and( + succeeded(), + ne(variables['Build.Reason'], 'PullRequest'), + or( + ne(dependencies.build.outputs['Windows.computeChannel.installerChannel'], 'stable'), + or( + eq(variables['Build.SourceBranch'], 'refs/heads/main'), + startsWith(variables['Build.SourceBranch'], 'refs/heads/release/') + ) + ) + ) + jobs: + - template: /eng/common/templates-official/jobs/jobs.yml@self + parameters: + enableMicrobuild: false + enablePublishUsingPipelines: false + enablePublishBuildAssets: false + enablePublishBuildArtifacts: true + enableTelemetry: true + workspace: + clean: all + jobs: + - job: WinGet + displayName: WinGet Manifest + timeoutInMinutes: 30 + pool: + name: NetCore1ESPool-Internal + image: 1es-windows-2022 + os: windows + steps: + - checkout: self + fetchDepth: 1 + - task: DownloadPipelineArtifact@2 + displayName: 🟣Download CLI archives + inputs: + itemPattern: | + **/aspire-cli-*.zip + targetPath: '$(Pipeline.Workspace)/native-archives' + - template: /eng/pipelines/templates/prepare-winget-manifest.yml@self + parameters: + version: $(aspireVersion) + artifactVersion: $(aspireArtifactVersion) + channel: $(installerChannel) + archiveRoot: $(Pipeline.Workspace)/native-archives + validateUrls: false + runInstallTest: false + + - job: Homebrew + displayName: Homebrew Cask + timeoutInMinutes: 30 + condition: and(succeeded(), eq(variables['installerChannel'], 'stable')) + pool: + name: Azure Pipelines + vmImage: macOS-latest-internal + os: macOS + steps: + - checkout: self + fetchDepth: 1 + - task: DownloadPipelineArtifact@2 + displayName: 🟣Download CLI archives + inputs: + itemPattern: | + **/aspire-cli-*.tar.gz + targetPath: '$(Pipeline.Workspace)/native-archives' + - template: /eng/pipelines/templates/prepare-homebrew-cask.yml@self + parameters: + version: $(aspireVersion) + artifactVersion: $(aspireArtifactVersion) + archiveRoot: $(Pipeline.Workspace)/native-archives + validateUrls: false + runInstallTest: false diff --git a/eng/pipelines/azure-pipelines.yml b/eng/pipelines/azure-pipelines.yml index 05e726b02fd..ca7f1de9845 100644 --- a/eng/pipelines/azure-pipelines.yml +++ b/eng/pipelines/azure-pipelines.yml @@ -227,6 +227,7 @@ extends: preSteps: - checkout: self fetchDepth: 1 + clean: true steps: - task: DownloadPipelineArtifact@2 @@ -295,23 +296,22 @@ extends: vscePublishPreRelease: ${{ parameters.vscePublishPreRelease }} # Extract the Aspire version from a generated nupkg filename so downstream - # stages (e.g. publish_winget) can use it. We use Aspire.Hosting.Docker - # because it has no RID in its filename, so stripping the prefix cleanly - # yields just the version (e.g. "13.2.0-preview.1.26074.3"). + # stages can use it. Aspire.Hosting.AppHost has no RID in its filename and + # does not suppress final package version stabilization, so stripping the + # prefix cleanly yields the stable version for installer publishing. - pwsh: | $ErrorActionPreference = 'Stop' $shippingDir = "$(Build.SourcesDirectory)/artifacts/packages/$(_BuildConfig)/Shipping" - $nupkg = Get-ChildItem -Path $shippingDir -Filter "Aspire.Hosting.Docker.*.nupkg" -ErrorAction SilentlyContinue | + $nupkg = Get-ChildItem -Path $shippingDir -Filter "Aspire.Hosting.AppHost.*.nupkg" -ErrorAction SilentlyContinue | Where-Object { $_.Name -notmatch '\.symbols\.' } | Select-Object -First 1 if (-not $nupkg) { - Write-Error "Could not find Aspire.Hosting.Docker nupkg in $shippingDir" + Write-Error "Could not find Aspire.Hosting.AppHost nupkg in $shippingDir" exit 1 } - # Extract version: strip the known prefix and .nupkg suffix - $version = $nupkg.Name -replace '^Aspire\.Hosting\.Docker\.', '' -replace '\.nupkg$', '' + $version = $nupkg.Name -replace '^Aspire\.Hosting\.AppHost\.', '' -replace '\.nupkg$', '' if ($version -notmatch '^\d+\.\d+\.\d+') { Write-Error "Extracted version '$version' does not look like a valid semver. Check the nupkg name." @@ -323,8 +323,22 @@ extends: name: computeVersion displayName: 🟣Compute Aspire version from nupkg - # Determine stable vs prerelease for installer pipelines (WinGet, Homebrew). - # DotNetFinalVersionKind is 'release' for stable builds, empty otherwise. + - pwsh: | + $ErrorActionPreference = 'Stop' + $artifactVersion = dotnet msbuild "$(Build.SourcesDirectory)/src/Aspire.Dashboard/Aspire.Dashboard.csproj" -getProperty:PackageVersion -property:SuppressFinalPackageVersion=true -property:OfficialBuildId=$(BUILD.BUILDNUMBER) + $artifactVersion = $artifactVersion.Trim() + + if ($artifactVersion -notmatch '^\d+\.\d+\.\d+') { + Write-Error "Computed artifact version '$artifactVersion' does not look like a valid semver. Check PackageVersion evaluation." + exit 1 + } + + Write-Host "Detected installer artifact version: $artifactVersion" + Write-Host "##vso[task.setvariable variable=aspireArtifactVersion;isOutput=true]$artifactVersion" + name: computeArtifactVersion + displayName: 🟣Compute installer artifact version + + # Determine stable vs prerelease for installer pipelines. - pwsh: | $ErrorActionPreference = 'Stop' $versionKind = dotnet msbuild "$(Build.SourcesDirectory)/eng/Versions.props" -getProperty:DotNetFinalVersionKind @@ -355,7 +369,6 @@ extends: # ---------------------------------------------------------------- # Generate and test installer packages (WinGet + Homebrew). - # Channel (stable/prerelease) is determined by the build stage. # Artifacts are consumed by release-publish-nuget.yml for stable # publishing. No publishing happens from this pipeline. # ---------------------------------------------------------------- @@ -365,8 +378,20 @@ extends: - build variables: aspireVersion: $[ stageDependencies.build.Windows.outputs['computeVersion.aspireVersion'] ] + aspireArtifactVersion: $[ stageDependencies.build.Windows.outputs['computeArtifactVersion.aspireArtifactVersion'] ] installerChannel: $[ stageDependencies.build.Windows.outputs['computeChannel.installerChannel'] ] - condition: and(succeeded(), ne(variables['Build.Reason'], 'PullRequest')) + condition: | + and( + succeeded(), + ne(variables['Build.Reason'], 'PullRequest'), + or( + ne(dependencies.build.outputs['Windows.computeChannel.installerChannel'], 'stable'), + or( + eq(variables['Build.SourceBranch'], 'refs/heads/main'), + startsWith(variables['Build.SourceBranch'], 'refs/heads/release/') + ) + ) + ) jobs: - template: /eng/common/templates-official/jobs/jobs.yml@self parameters: @@ -391,11 +416,13 @@ extends: - template: /eng/pipelines/templates/prepare-winget-manifest.yml@self parameters: version: $(aspireVersion) + artifactVersion: $(aspireArtifactVersion) channel: $(installerChannel) - job: Homebrew displayName: Homebrew Cask timeoutInMinutes: 30 + condition: and(succeeded(), eq(variables['installerChannel'], 'stable')) pool: name: Azure Pipelines vmImage: macOS-latest-internal @@ -406,4 +433,4 @@ extends: - template: /eng/pipelines/templates/prepare-homebrew-cask.yml@self parameters: version: $(aspireVersion) - channel: $(installerChannel) + artifactVersion: $(aspireArtifactVersion) diff --git a/eng/pipelines/release-publish-nuget.yml b/eng/pipelines/release-publish-nuget.yml index f19f7176d61..ebc243fbf85 100644 --- a/eng/pipelines/release-publish-nuget.yml +++ b/eng/pipelines/release-publish-nuget.yml @@ -115,14 +115,16 @@ extends: Write-Host "Source Build ID: $(resources.pipeline.aspire-build.runID)" Write-Host "Source Build Name: $(resources.pipeline.aspire-build.runName)" Write-Host "This stage downloads artifacts and re-publishes them so 1ES PT can generate SBOM." + Write-Host "Installer-only mode: ${{ and(eq(parameters.SkipNuGetPublish, true), eq(parameters.SkipChannelPromotion, true)) }}" Write-Host "===============================" displayName: 'Log Stage Info' # Download artifacts from the source build pipeline - - download: aspire-build - displayName: 'Download PackageArtifacts from Source Build' - artifact: PackageArtifacts - patterns: '**/*.nupkg' + - ${{ if not(and(eq(parameters.SkipNuGetPublish, true), eq(parameters.SkipChannelPromotion, true))) }}: + - download: aspire-build + displayName: 'Download PackageArtifacts from Source Build' + artifact: PackageArtifacts + patterns: '**/*.nupkg' - download: aspire-build displayName: 'Download WinGet Manifests from Source Build' @@ -133,27 +135,39 @@ extends: artifact: homebrew-cask-stable # Move artifacts to expected location for output - - powershell: | - $sourcePath = "$(Pipeline.Workspace)/aspire-build/PackageArtifacts" - $targetPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" - - Write-Host "Moving artifacts from $sourcePath to $targetPath" - - if (!(Test-Path $targetPath)) { - New-Item -ItemType Directory -Path $targetPath -Force | Out-Null - } - - # Copy all nupkg files - $packages = Get-ChildItem -Path $sourcePath -Filter "*.nupkg" -Recurse - Write-Host "Found $($packages.Count) packages to copy" - - foreach ($pkg in $packages) { - Copy-Item $pkg.FullName -Destination $targetPath -Force - Write-Host " Copied: $($pkg.Name)" - } - - Write-Host "✓ Artifacts prepared for SBOM generation" - displayName: 'Prepare Artifacts for Publishing' + - ${{ if not(and(eq(parameters.SkipNuGetPublish, true), eq(parameters.SkipChannelPromotion, true))) }}: + - powershell: | + $sourcePath = "$(Pipeline.Workspace)/aspire-build/PackageArtifacts" + $targetPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" + + Write-Host "Moving artifacts from $sourcePath to $targetPath" + + if (!(Test-Path $targetPath)) { + New-Item -ItemType Directory -Path $targetPath -Force | Out-Null + } + + # Copy all nupkg files + $packages = Get-ChildItem -Path $sourcePath -Filter "*.nupkg" -Recurse + Write-Host "Found $($packages.Count) packages to copy" + + foreach ($pkg in $packages) { + Copy-Item $pkg.FullName -Destination $targetPath -Force + Write-Host " Copied: $($pkg.Name)" + } + + Write-Host "✓ Artifacts prepared for SBOM generation" + displayName: 'Prepare Artifacts for Publishing' + + - ${{ if and(eq(parameters.SkipNuGetPublish, true), eq(parameters.SkipChannelPromotion, true)) }}: + - powershell: | + $targetPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" + + if (!(Test-Path $targetPath)) { + New-Item -ItemType Directory -Path $targetPath -Force | Out-Null + } + + Write-Host "Installer-only run detected; created empty PackageArtifacts placeholder." + displayName: 'Prepare Empty PackageArtifacts Placeholder' # Copy installer artifacts to expected locations for output - powershell: | @@ -225,118 +239,121 @@ extends: Write-Host "Dry Run: ${{ parameters.DryRun }}" Write-Host "Skip NuGet Publish: ${{ parameters.SkipNuGetPublish }}" Write-Host "Skip Channel Promotion: ${{ parameters.SkipChannelPromotion }}" + Write-Host "Installer-only mode: ${{ and(eq(parameters.SkipNuGetPublish, true), eq(parameters.SkipChannelPromotion, true)) }}" Write-Host "===================================" displayName: 'Validate Parameters' # ===== EXTRACT BAR BUILD ID ===== - - powershell: | - $buildId = "$(resources.pipeline.aspire-build.runID)" - $org = "$(System.CollectionUri)" - $project = "internal" + - ${{ if eq(parameters.SkipChannelPromotion, false) }}: + - powershell: | + $buildId = "$(resources.pipeline.aspire-build.runID)" + $org = "$(System.CollectionUri)" + $project = "internal" - Write-Host "Fetching build tags for build: $buildId" + Write-Host "Fetching build tags for build: $buildId" - # Use Azure DevOps REST API to get build tags - $uri = "${org}${project}/_apis/build/builds/${buildId}/tags?api-version=7.0" - Write-Host "API URI: $uri" + # Use Azure DevOps REST API to get build tags + $uri = "${org}${project}/_apis/build/builds/${buildId}/tags?api-version=7.0" + Write-Host "API URI: $uri" - try { - $response = Invoke-RestMethod -Uri $uri -Headers @{ - Authorization = "Bearer $(System.AccessToken)" - } -Method Get + try { + $response = Invoke-RestMethod -Uri $uri -Headers @{ + Authorization = "Bearer $(System.AccessToken)" + } -Method Get - Write-Host "Build tags found: $($response.value -join ', ')" + Write-Host "Build tags found: $($response.value -join ', ')" - # Find the BAR ID tag - $barIdTag = $response.value | Where-Object { $_ -match 'BAR ID - (\d+)' } + # Find the BAR ID tag + $barIdTag = $response.value | Where-Object { $_ -match 'BAR ID - (\d+)' } - if ($barIdTag -and $barIdTag -match 'BAR ID - (\d+)') { - $barBuildId = $Matches[1] - Write-Host "✓ Extracted BAR Build ID: $barBuildId" - Write-Host "##vso[task.setvariable variable=BarBuildId]$barBuildId" - } else { - Write-Error "Could not find BAR ID tag in build $buildId. Tags found: $($response.value -join ', ')" + if ($barIdTag -and $barIdTag -match 'BAR ID - (\d+)') { + $barBuildId = $Matches[1] + Write-Host "✓ Extracted BAR Build ID: $barBuildId" + Write-Host "##vso[task.setvariable variable=BarBuildId]$barBuildId" + } else { + Write-Error "Could not find BAR ID tag in build $buildId. Tags found: $($response.value -join ', ')" + exit 1 + } + } catch { + Write-Error "Failed to fetch build tags: $_" exit 1 } - } catch { - Write-Error "Failed to fetch build tags: $_" - exit 1 - } - displayName: 'Extract BAR Build ID' - env: - SYSTEM_ACCESSTOKEN: $(System.AccessToken) + displayName: 'Extract BAR Build ID' + env: + SYSTEM_ACCESSTOKEN: $(System.AccessToken) # ===== DOWNLOAD PACKAGES ===== # Artifacts are downloaded automatically via templateContext.inputs above - - task: UseDotNet@2 - displayName: 'Install .NET 10 SDK' - inputs: - packageType: 'sdk' - version: '10.0.x' + - ${{ if eq(parameters.SkipNuGetPublish, false) }}: + - task: UseDotNet@2 + displayName: 'Install .NET 10 SDK' + inputs: + packageType: 'sdk' + version: '10.0.x' - - powershell: | - $packagesPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" - Write-Host "=== Package Inventory ===" + - powershell: | + $packagesPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" + Write-Host "=== Package Inventory ===" - $packages = Get-ChildItem -Path $packagesPath -Filter "*.nupkg" -Recurse - Write-Host "Found $($packages.Count) packages:" + $packages = Get-ChildItem -Path $packagesPath -Filter "*.nupkg" -Recurse + Write-Host "Found $($packages.Count) packages:" - foreach ($pkg in $packages) { - $sizeMB = [math]::Round($pkg.Length / 1MB, 2) - Write-Host " - $($pkg.Name) ($sizeMB MB)" - } + foreach ($pkg in $packages) { + $sizeMB = [math]::Round($pkg.Length / 1MB, 2) + Write-Host " - $($pkg.Name) ($sizeMB MB)" + } - if ($packages.Count -eq 0) { - Write-Error "No packages found in artifacts!" - exit 1 - } + if ($packages.Count -eq 0) { + Write-Error "No packages found in artifacts!" + exit 1 + } - Write-Host "===========================" - displayName: 'List Packages' + Write-Host "===========================" + displayName: 'List Packages' - # ===== VERIFY SIGNATURES ===== - - powershell: | - $packagesPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" - Write-Host "=== Verifying Package Signatures ===" - - $packages = Get-ChildItem -Path $packagesPath -Filter "*.nupkg" -Recurse - $failedVerification = @() - - foreach ($package in $packages) { - Write-Host "Verifying: $($package.Name)" - - # Use $ErrorActionPreference to prevent PowerShell from treating stderr as terminating error - $originalErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = 'Continue' - $result = dotnet nuget verify $package.FullName 2>&1 - $verifyExitCode = $LASTEXITCODE - $ErrorActionPreference = $originalErrorActionPreference - - if ($verifyExitCode -ne 0) { - Write-Host " ❌ Signature verification FAILED" - Write-Host $result - $failedVerification += $package.Name - } else { - Write-Host " ✓ Signature valid" + # ===== VERIFY SIGNATURES ===== + - powershell: | + $packagesPath = "$(Pipeline.Workspace)/packages/PackageArtifacts" + Write-Host "=== Verifying Package Signatures ===" + + $packages = Get-ChildItem -Path $packagesPath -Filter "*.nupkg" -Recurse + $failedVerification = @() + + foreach ($package in $packages) { + Write-Host "Verifying: $($package.Name)" + + # Use $ErrorActionPreference to prevent PowerShell from treating stderr as terminating error + $originalErrorActionPreference = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + $result = dotnet nuget verify $package.FullName 2>&1 + $verifyExitCode = $LASTEXITCODE + $ErrorActionPreference = $originalErrorActionPreference + + if ($verifyExitCode -ne 0) { + Write-Host " ❌ Signature verification FAILED" + Write-Host $result + $failedVerification += $package.Name + } else { + Write-Host " ✓ Signature valid" + } } - } - if ($failedVerification.Count -gt 0) { - Write-Host "" - Write-Host "=== SIGNATURE VERIFICATION FAILED ===" - Write-Host "The following packages failed signature verification:" - foreach ($pkg in $failedVerification) { - Write-Host " - $pkg" + if ($failedVerification.Count -gt 0) { + Write-Host "" + Write-Host "=== SIGNATURE VERIFICATION FAILED ===" + Write-Host "The following packages failed signature verification:" + foreach ($pkg in $failedVerification) { + Write-Host " - $pkg" + } + Write-Error "Package signature verification failed. Aborting release." + exit 1 } - Write-Error "Package signature verification failed. Aborting release." - exit 1 - } - Write-Host "" - Write-Host "✓ All $($packages.Count) packages passed signature verification" - Write-Host "===========================" - displayName: 'Verify Package Signatures' + Write-Host "" + Write-Host "✓ All $($packages.Count) packages passed signature verification" + Write-Host "===========================" + displayName: 'Verify Package Signatures' # ===== PUBLISH TO NUGET ===== # Dry Run: List packages that would be published (skip actual publish) @@ -450,7 +467,11 @@ extends: Write-Host "╠═══════════════════════════════════════════════════════════════╣" Write-Host "║ Source Build: $(resources.pipeline.aspire-build.runName)" Write-Host "║ Source Build ID: $(resources.pipeline.aspire-build.runID)" - Write-Host "║ BAR Build ID: $(BarBuildId)" + if ("${{ parameters.SkipChannelPromotion }}" -eq "true") { + Write-Host "║ BAR Build ID: (SKIPPED)" + } else { + Write-Host "║ BAR Build ID: $(BarBuildId)" + } Write-Host "║ GA Channel: ${{ parameters.GaChannelName }}" Write-Host "║ Dry Run: ${{ parameters.DryRun }}" Write-Host "╠═══════════════════════════════════════════════════════════════╣" @@ -526,5 +547,4 @@ extends: - template: /eng/pipelines/templates/publish-homebrew.yml@self parameters: caskArtifactPath: $(Pipeline.Workspace)/homebrew/homebrew-cask-stable - channel: stable dryRun: ${{ parameters.DryRun }} diff --git a/eng/pipelines/templates/prepare-homebrew-cask.yml b/eng/pipelines/templates/prepare-homebrew-cask.yml index 7d17038fd01..8273527099a 100644 --- a/eng/pipelines/templates/prepare-homebrew-cask.yml +++ b/eng/pipelines/templates/prepare-homebrew-cask.yml @@ -2,116 +2,153 @@ parameters: - name: version type: string default: '' - - name: channel + - name: artifactVersion type: string + default: '' + - name: archiveRoot + type: string + default: '' + - name: validateUrls + type: boolean + default: true + - name: runInstallTest + type: boolean + default: true steps: - bash: | set -euo pipefail version='${{ parameters.version }}' - channel='${{ parameters.channel }}' + artifactVersion='${{ parameters.artifactVersion }}' if [ -z "$version" ]; then echo "##[error]Version parameter is required" exit 1 fi - if [ -z "$channel" ]; then - echo "##[error]Channel parameter is required (stable or prerelease)" - exit 1 - fi - - if [ "$channel" != "stable" ] && [ "$channel" != "prerelease" ]; then - echo "##[error]Channel must be 'stable' or 'prerelease', got: $channel" - exit 1 - fi - echo "Version: $version" - echo "Channel: $channel" + echo "Artifact version: ${artifactVersion:-$version}" echo "##vso[task.setvariable variable=CliVersion]$version" - echo "##vso[task.setvariable variable=CliChannel]$channel" + echo "##vso[task.setvariable variable=CliArtifactVersion]${artifactVersion:-$version}" displayName: 🟣Set version ${{ parameters.version }} - bash: | set -euo pipefail version="$(CliVersion)" - channel="$(CliChannel)" + artifactVersion="$(CliArtifactVersion)" outputDir="$(Build.StagingDirectory)/homebrew-cask" mkdir -p "$outputDir" + outputFile="$outputDir/aspire.rb" - if [ "$channel" = "prerelease" ]; then - outputFile="$outputDir/aspire@prerelease.rb" - else - outputFile="$outputDir/aspire.rb" + echo "Generating Homebrew cask for Aspire version $version" + + args=( + --version "$version" + --artifact-version "$artifactVersion" + --output "$outputFile" + ) + + if [ -n "${ARCHIVE_ROOT:-}" ]; then + args+=(--archive-root "$ARCHIVE_ROOT") fi - echo "Generating Homebrew cask for Aspire version $version (channel: $channel)" + validateUrlsNormalized="$(printf '%s' "${VALIDATE_URLS:-false}" | tr '[:upper:]' '[:lower:]')" + if [ "$validateUrlsNormalized" = "true" ]; then + args+=(--validate-urls) + fi - "$(Build.SourcesDirectory)/eng/homebrew/generate-cask.sh" \ - --version "$version" \ - --channel "$channel" \ - --output "$outputFile" \ - --validate-urls + "$(Build.SourcesDirectory)/eng/homebrew/generate-cask.sh" "${args[@]}" echo "" echo "Generated cask:" cat "$outputFile" displayName: 🟣Generate Homebrew cask + env: + ARCHIVE_ROOT: '${{ parameters.archiveRoot }}' + VALIDATE_URLS: '${{ parameters.validateUrls }}' - bash: | set -euo pipefail - channel="$(CliChannel)" + caskFile="aspire.rb" + caskName="aspire" + caskPath="$(Build.StagingDirectory)/homebrew-cask/$caskFile" + firstLetter="${caskName:0:1}" + targetPath="Casks/$firstLetter/$caskFile" + tapName="local/aspire" + tapRoot="$(brew --repository)/Library/Taps/local/homebrew-aspire" - if [ "$channel" = "prerelease" ]; then - caskFile="aspire@prerelease.rb" - caskName="aspire@prerelease" - else - caskFile="aspire.rb" - caskName="aspire" - fi + cleanup() { + brew untap "$tapName" >/dev/null 2>&1 || true + } - caskPath="$(Build.StagingDirectory)/homebrew-cask/$caskFile" + trap cleanup EXIT echo "Validating Ruby syntax..." ruby -c "$caskPath" echo "Ruby syntax OK" + brew tap-new --no-git "$tapName" + mkdir -p "$tapRoot/Casks" + cp "$caskPath" "$tapRoot/Casks/$caskFile" + tappedCaskPath="$tapRoot/Casks/$caskFile" + + echo "" + echo "Applying Homebrew style fixes in local tap..." + brew style --fix "$tappedCaskPath" + cp "$tappedCaskPath" "$caskPath" + echo "Homebrew style OK" + echo "" echo "Auditing cask via local tap..." - brew tap-new --no-git local/aspire - mkdir -p "$(brew --repository)/Library/Taps/local/homebrew-aspire/Casks" - cp "$caskPath" "$(brew --repository)/Library/Taps/local/homebrew-aspire/Casks/$caskFile" - auditCode=0 - brew audit --cask "local/aspire/$caskName" || auditCode=$? + upstreamStatusCode="$(curl -sS -o /dev/null -w "%{http_code}" "https://api.github.com/repos/Homebrew/homebrew-cask/contents/$targetPath")" + auditArgs=(--cask --online "$tapName/$caskName") + auditCommand="brew audit --cask --online $tapName/$caskName" + isNewCask=false + if [ "$upstreamStatusCode" = "404" ]; then + echo "Detected new upstream cask; running new-cask audit." + auditArgs=(--cask --new --online "$tapName/$caskName") + auditCommand="brew audit --cask --new --online $tapName/$caskName" + isNewCask=true + elif [ "$upstreamStatusCode" = "200" ]; then + echo "Detected existing upstream cask; running standard online audit." + else + echo "##[error]Could not determine whether $targetPath exists upstream (HTTP $upstreamStatusCode)" + exit 1 + fi - brew untap local/aspire + auditCode=0 + brew audit "${auditArgs[@]}" || auditCode=$? if [ $auditCode -ne 0 ]; then echo "##[error]brew audit failed" exit $auditCode fi + echo "##vso[task.setvariable variable=HomebrewAuditCommand]$auditCommand" + echo "##vso[task.setvariable variable=HomebrewIsNewCask]$isNewCask" echo "brew audit passed" + trap - EXIT + cleanup displayName: 🟣Validate cask syntax and audit - bash: | set -euo pipefail - channel="$(CliChannel)" - - if [ "$channel" = "prerelease" ]; then - caskFile="aspire@prerelease.rb" - caskName="aspire@prerelease" - else - caskFile="aspire.rb" - caskName="aspire" - fi - + caskFile="aspire.rb" + caskName="aspire" caskPath="$(Build.StagingDirectory)/homebrew-cask/$caskFile" + tapName="local/aspire-test" + tapRoot="$(brew --repository)/Library/Taps/local/homebrew-aspire-test" echo "Testing cask install/uninstall: $caskPath" echo "" + cleanup() { + brew untap "$tapName" >/dev/null 2>&1 || true + } + + trap cleanup EXIT + # Verify aspire is NOT already installed echo "Verifying aspire is not already installed..." if command -v aspire &>/dev/null; then @@ -123,15 +160,14 @@ steps: # Set up a local tap so brew accepts the cask echo "" echo "Setting up local tap..." - brew tap-new --no-git local/aspire-test - tapCaskDir="$(brew --repository)/Library/Taps/local/homebrew-aspire-test/Casks" - mkdir -p "$tapCaskDir" - cp "$caskPath" "$tapCaskDir/$caskFile" + brew tap-new --no-git "$tapName" + mkdir -p "$tapRoot/Casks" + cp "$caskPath" "$tapRoot/Casks/$caskFile" # Install from local tap echo "" echo "Installing aspire from local tap..." - brew install --cask "local/aspire-test/$caskName" + HOMEBREW_NO_INSTALL_FROM_API=1 brew install --cask "$tapName/$caskName" echo "✅ Install succeeded" # Verify aspire is now available and runs @@ -140,32 +176,73 @@ steps: if ! command -v aspire &>/dev/null; then echo "##[error]aspire command not found in PATH after install" # Show brew info for diagnostics - brew info --cask "local/aspire-test/$caskName" || true - brew untap local/aspire-test + brew info --cask "$tapName/$caskName" || true exit 1 fi echo " Path: $(command -v aspire)" - aspireVersion="$(aspire --version 2>&1)" || true + aspireVersion="$(aspire --version 2>&1)" echo " Version: $aspireVersion" echo "✅ aspire CLI verified" # Uninstall echo "" echo "Uninstalling aspire..." - brew uninstall --cask "local/aspire-test/$caskName" + brew uninstall --cask "$tapName/$caskName" echo "✅ Uninstall succeeded" # Clean up tap - brew untap local/aspire-test + trap - EXIT + cleanup # Verify aspire is removed if command -v aspire &>/dev/null; then - echo "##[warning]aspire command still found in PATH after uninstall" + echo "##[error]aspire command still found in PATH after uninstall" + exit 1 else echo " Confirmed: aspire is no longer in PATH" fi displayName: 🟣Test cask install/uninstall + condition: and(succeeded(), eq('${{ parameters.runInstallTest }}', 'true')) + + - bash: | + set -euo pipefail + outputPath="$(Build.StagingDirectory)/homebrew-cask/validation-summary.json" + cat > "$outputPath" <$null - if ($LASTEXITCODE -ne 0) { - git clone "https://github.com/${botUser}/homebrew-cask.git" $repoDir + Write-Host "Waiting for fork creation ($attempt/12)..." + } + + if (-not $forkReady) { + Write-Error "Timed out waiting for fork $botUser/homebrew-cask to become available." + exit 1 + } + } else { + Write-Host "Fork already exists: $botUser/homebrew-cask" + } + + $branchName = "$($caskName -replace '@', '-')-$version" + $firstLetter = $caskName.Substring(0, 1) + $targetPath = "Casks/$firstLetter/$caskFile" + $targetPathForUri = "Casks/$firstLetter/$([Uri]::EscapeDataString($caskFile))" + $upstreamBranch = Invoke-GitHubApi -Method Get -Uri "https://api.github.com/repos/Homebrew/homebrew-cask/branches/$defaultBranchForUri" -Body $null + $upstreamSha = $upstreamBranch.commit.sha + $caskContent = (Get-Content -Path $caskPath -Raw) -replace "`r`n", "`n" + if (-not $caskContent.EndsWith("`n")) { + $caskContent += "`n" } - Push-Location $repoDir + $upstreamContent = $null try { - git remote add upstream https://github.com/Homebrew/homebrew-cask.git 2>$null - git fetch upstream master - git checkout -B "aspire-${version}" upstream/master - - # Casks are organized by first letter: Casks/a/aspire.rb - $firstLetter = $caskName.Substring(0, 1) - $targetDir = "Casks/$firstLetter" - New-Item -ItemType Directory -Path $targetDir -Force | Out-Null - Copy-Item $caskPath "$targetDir/$caskFile" - - git config user.name "dotnet-bot" - git config user.email "dotnet-bot@microsoft.com" - git add "$targetDir/$caskFile" - git commit -m "$caskName $version" - git push --force origin "aspire-${version}" - - # Create PR (or update existing) - $existingPR = gh pr list --repo Homebrew/homebrew-cask --head "${botUser}:aspire-${version}" --json number --jq '.[0].number' 2>$null - if ($existingPR) { - Write-Host "PR #$existingPR already exists — updated branch" - } else { - gh pr create ` - --repo Homebrew/homebrew-cask ` - --title "$caskName $version" ` - --body "Update $caskName to version $version." ` - --head "${botUser}:aspire-${version}" - Write-Host "PR created successfully" - } - } finally { - Pop-Location + $upstreamFile = Invoke-GitHubApi -Method Get -Uri "https://api.github.com/repos/Homebrew/homebrew-cask/contents/${targetPathForUri}?ref=$defaultBranchForUri" -Body $null + if ($upstreamFile.content) { + $upstreamContent = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String(($upstreamFile.content -replace '\s', ''))) -replace "`r`n", "`n" + } + } + catch { + $statusCode = Get-StatusCode -ErrorRecord $_ + if ($statusCode -ne 404) { + throw + } + } + + if ($upstreamContent -eq $caskContent) { + Write-Host "No Homebrew changes to submit; upstream already matches $caskName $version." + exit 0 + } + + $branchRefUri = "https://api.github.com/repos/$botUser/homebrew-cask/git/refs/heads/$branchName" + try { + Invoke-GitHubApi -Method Get -Uri $branchRefUri -Body $null | Out-Null + Invoke-GitHubApi -Method Patch -Uri $branchRefUri -Body @{ + sha = $upstreamSha + force = $true + } | Out-Null + Write-Host "Reset branch $branchName to upstream/$defaultBranch" + } + catch { + $statusCode = Get-StatusCode -ErrorRecord $_ + if ($statusCode -eq 404) { + Invoke-GitHubApi -Method Post -Uri "https://api.github.com/repos/$botUser/homebrew-cask/git/refs" -Body @{ + ref = "refs/heads/$branchName" + sha = $upstreamSha + } | Out-Null + Write-Host "Created branch $branchName from upstream/$defaultBranch" + } + else { + throw + } + } + + $branchFileUri = "https://api.github.com/repos/$botUser/homebrew-cask/contents/${targetPathForUri}?ref=$([Uri]::EscapeDataString($branchName))" + $existingBranchFileSha = $null + try { + $branchFile = Invoke-GitHubApi -Method Get -Uri $branchFileUri -Body $null + $existingBranchFileSha = $branchFile.sha + } + catch { + $statusCode = Get-StatusCode -ErrorRecord $_ + if ($statusCode -ne 404) { + throw + } + } + + $putBody = @{ + message = "$caskName $version" + content = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes($caskContent)) + branch = $branchName + } + + if ($existingBranchFileSha) { + $putBody.sha = $existingBranchFileSha + } + + $contentResult = Invoke-GitHubApi -Method Put -Uri "https://api.github.com/repos/$botUser/homebrew-cask/contents/$targetPathForUri" -Body $putBody + Write-Host "Updated $targetPath on branch $branchName with commit $($contentResult.commit.sha)" + + $compareUri = "https://api.github.com/repos/Homebrew/homebrew-cask/compare/$defaultBranchForUri...$([Uri]::EscapeDataString("${botUser}:$branchName"))" + $compareResult = $null + for ($attempt = 1; $attempt -le 10; $attempt++) { + try { + $compareResult = Invoke-GitHubApi -Method Get -Uri $compareUri -Body $null + if ($compareResult.ahead_by -gt 0) { + break + } + + Write-Host "Waiting for branch comparison to reflect new commit ($attempt/10)..." + } + catch { + $statusCode = Get-StatusCode -ErrorRecord $_ + Write-Host "Branch comparison is not ready yet ($statusCode) ($attempt/10)" + } + + Start-Sleep -Seconds 3 + } + + if ($null -eq $compareResult -or $compareResult.ahead_by -le 0) { + Write-Error "Branch $branchName does not contain commits ahead of Homebrew/homebrew-cask:$defaultBranch, so a PR cannot be created." + exit 1 + } + + Write-Host "Branch comparison status: $($compareResult.status); ahead_by=$($compareResult.ahead_by)" + + # Create PR (or update existing) + $encodedHead = [Uri]::EscapeDataString("${botUser}:$branchName") + $existingPRsResponse = Invoke-GitHubApi -Method Get -Uri "https://api.github.com/repos/Homebrew/homebrew-cask/pulls?head=$encodedHead&state=all" -Body $null + $existingPRs = @( + foreach ($existingPr in $existingPRsResponse) { + if ($null -ne $existingPr) { + $existingPr + } + } + ) + $existingOpenPr = $existingPRs | Where-Object { $_.state -eq 'open' } | Select-Object -First 1 + $existingClosedPrs = @($existingPRs | Where-Object { $_.state -ne 'open' } | Sort-Object updated_at -Descending) + $supersededPullRequestNumbers = @( + foreach ($closedPr in $existingClosedPrs) { + [Convert]::ToInt32("$($closedPr.number)", [System.Globalization.CultureInfo]::InvariantCulture) + } + ) + $isStableCask = [bool]$validationSummary.isStableRelease + $pullRequestBody = Get-HomebrewPullRequestBody -Version $version -CaskName $caskName -CaskFile $caskFile -IsStable $isStableCask -ValidationSummary $validationSummary -SupersededPullRequestNumbers $supersededPullRequestNumbers + + if ($existingOpenPr) { + $updatedPr = Invoke-GitHubApi -Method Patch -Uri "https://api.github.com/repos/Homebrew/homebrew-cask/pulls/$($existingOpenPr.number)" -Body @{ + title = "$caskName $version" + body = $pullRequestBody + } + + Write-Host "PR #$($existingOpenPr.number) already exists — updated title/body at $($updatedPr.html_url)" + + if (-not [bool]$existingOpenPr.draft) { + Write-Host "Converting existing PR #$($existingOpenPr.number) to draft..." + + $graphQlData = Invoke-GitHubGraphQL -Query @' + mutation ConvertPullRequestToDraft($pullRequestId: ID!) { + convertPullRequestToDraft(input: { pullRequestId: $pullRequestId }) { + pullRequest { + number + isDraft + url + } + } + } + '@ -Variables @{ + pullRequestId = $existingOpenPr.node_id + } + + if (-not $graphQlData.convertPullRequestToDraft.pullRequest.isDraft) { + throw "Failed to convert PR #$($existingOpenPr.number) to draft." + } + + Write-Host "PR #$($existingOpenPr.number) is now draft." + } + } else { + if ($existingClosedPrs.Count -gt 0) { + $closedPrSummary = ($existingClosedPrs | ForEach-Object { "#$($_.number)" }) -join ', ' + Write-Host "Found prior closed PRs for ${botUser}:${branchName}: $closedPrSummary" + } + + $createdPr = Invoke-GitHubApi -Method Post -Uri 'https://api.github.com/repos/Homebrew/homebrew-cask/pulls' -Body @{ + title = "$caskName $version" + body = $pullRequestBody + head = "${botUser}:$branchName" + base = $defaultBranch + draft = $true + } + + Write-Host "Draft PR created successfully: #$($createdPr.number) $($createdPr.html_url)" } displayName: '🟣Submit PR to Homebrew/homebrew-cask' condition: and(succeeded(), eq('${{ parameters.dryRun }}', 'false')) diff --git a/eng/pipelines/templates/publish-winget.yml b/eng/pipelines/templates/publish-winget.yml index 74dc0cf78b4..780e6cbac38 100644 --- a/eng/pipelines/templates/publish-winget.yml +++ b/eng/pipelines/templates/publish-winget.yml @@ -21,6 +21,7 @@ steps: - powershell: | $ErrorActionPreference = 'Stop' $manifestRoot = '${{ parameters.manifestArtifactPath }}' + $packageId = '${{ parameters.packageIdentifier }}' Write-Host "=== WinGet Manifests ===" Get-ChildItem -Path $manifestRoot -Recurse | Format-Table FullName @@ -35,14 +36,36 @@ steps: exit 1 } - Write-Host "Manifest directory: $manifestDir" - Write-Host "##vso[task.setvariable variable=WinGetManifestDir]$manifestDir" + $submissionDir = Join-Path '$(Build.StagingDirectory)' 'winget-submit-manifests' + if (Test-Path $submissionDir) { + Remove-Item -Path $submissionDir -Recurse -Force + } + + New-Item -ItemType Directory -Path $submissionDir -Force | Out-Null + Get-ChildItem -Path $manifestDir -File -Filter "*.yaml" | ForEach-Object { + Copy-Item -Path $_.FullName -Destination (Join-Path $submissionDir $_.Name) -Force + } - # Extract version from manifest directory name (e.g., .../Microsoft.Aspire/13.2.0/) + Write-Host "Manifest directory: $submissionDir" + Write-Host "##vso[task.setvariable variable=WinGetManifestDir]$submissionDir" + + $versionManifestPath = Join-Path $submissionDir "$packageId.yaml" + + # Extract version from the version manifest when no explicit version is provided. $version = '${{ parameters.version }}' if ([string]::IsNullOrWhiteSpace($version)) { - $version = Split-Path $manifestDir -Leaf - Write-Host "Extracted version from directory: $version" + if (Test-Path $versionManifestPath) { + $versionMatch = Select-String -Path $versionManifestPath -Pattern '^\s*PackageVersion:\s*(.+)\s*$' | Select-Object -First 1 + if ($versionMatch) { + $version = $versionMatch.Matches[0].Groups[1].Value.Trim() + Write-Host "Extracted version from version manifest: $version" + } + } + + if ([string]::IsNullOrWhiteSpace($version)) { + $version = Split-Path $manifestDir -Leaf + Write-Host "Fell back to directory name for version: $version" + } } else { Write-Host "Using provided version: $version" } @@ -160,8 +183,30 @@ steps: Write-Host "Submitting WinGet manifests for $packageId version $version" Write-Host "Manifest directory: $manifestDir" Write-Host "Manifest files:" - Get-ChildItem -Path $manifestDir -Filter "*.yaml" | ForEach-Object { Write-Host " - $($_.Name)" } + Get-ChildItem -Path $manifestDir -File -Filter "*.yaml" | Sort-Object Name | ForEach-Object { Write-Host " - $($_.Name)" } + + $versionManifest = Join-Path $manifestDir "$packageId.yaml" + $installerManifest = Join-Path $manifestDir "$packageId.installer.yaml" + $localeManifests = Get-ChildItem -Path $manifestDir -File -Filter "$packageId.locale.*.yaml" | Sort-Object Name + + $missingManifests = @() + foreach ($requiredManifest in @($versionManifest, $installerManifest)) { + if (-not (Test-Path $requiredManifest)) { + $missingManifests += $requiredManifest + } + } + + if ($localeManifests.Count -eq 0) { + Write-Error "No locale manifests were found for $packageId in $manifestDir" + exit 1 + } + + if ($missingManifests.Count -gt 0) { + Write-Error "Missing required manifest files: $($missingManifests -join ', ')" + exit 1 + } + Write-Host "Submitting clean manifest directory to wingetcreate: $manifestDir" $output = & "$(Build.StagingDirectory)/wingetcreate.exe" submit $manifestDir ` --token $token ` --prtitle "Update $packageId to version $version" 2>&1 diff --git a/eng/refreshTypeScriptSdks.ps1 b/eng/refreshTypeScriptSdks.ps1 new file mode 100644 index 00000000000..1d780c1d39d --- /dev/null +++ b/eng/refreshTypeScriptSdks.ps1 @@ -0,0 +1,152 @@ +#!/usr/bin/env pwsh + +[CmdletBinding()] +param( + [string]$AppPattern = '*' +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Stop' +$PSNativeCommandUseErrorActionPreference = $true + +$scriptDir = $PSScriptRoot +$repoRoot = Split-Path -Parent $scriptDir +$playgroundRoot = Join-Path -Path $repoRoot -ChildPath 'playground/polyglot/TypeScript' +$cliProject = Join-Path -Path $repoRoot -ChildPath 'src/Aspire.Cli/Aspire.Cli.csproj' +$requiredGeneratedFiles = @('aspire.ts', 'base.ts', 'transport.ts') + +function Invoke-RepoRestore { + if ($IsWindows -or $env:OS -eq 'Windows_NT') { + & (Join-Path $repoRoot 'restore.cmd') + } + else { + & (Join-Path $repoRoot 'restore.sh') + } +} + +function Get-ValidationAppHosts { + if (-not (Test-Path $playgroundRoot)) { + throw "TypeScript playground directory not found at '$playgroundRoot'." + } + + $appHosts = foreach ($integrationDir in Get-ChildItem -Path $playgroundRoot -Directory | Sort-Object Name) { + if ($integrationDir.Name -notlike $AppPattern) { + continue + } + + $appHostDir = Join-Path $integrationDir.FullName 'ValidationAppHost' + $appHostEntryPoint = Join-Path $appHostDir 'apphost.ts' + if (Test-Path $appHostEntryPoint) { + Get-Item $appHostDir + } + } + + if (@($appHosts).Count -eq 0) { + throw "No TypeScript playground ValidationAppHost directories matched '$AppPattern'." + } + + return @($appHosts) +} + +function Install-NodeDependencies([string]$appDir) { + Push-Location $appDir + try { + $packageLockPath = Join-Path $appDir 'package-lock.json' + if (Test-Path $packageLockPath) { + & npm ci --ignore-scripts --no-audit --no-fund + } + else { + & npm install --ignore-scripts --no-audit --no-fund + } + } + finally { + Pop-Location + } +} + +function Assert-GeneratedSdkFiles([string]$appDir) { + $generatedDir = Join-Path $appDir '.modules' + foreach ($file in $requiredGeneratedFiles) { + $path = Join-Path $generatedDir $file + if (-not (Test-Path $path)) { + throw "Expected generated SDK file '$path' was not created." + } + } +} + +if (-not (Get-Command dotnet -ErrorAction SilentlyContinue)) { + throw "The .NET SDK was not found in PATH." +} + +if (-not (Get-Command npm -ErrorAction SilentlyContinue)) { + throw "npm was not found in PATH." +} + +Write-Host '=== Refreshing TypeScript playground SDKs ===' +Write-Host "Playground root: $playgroundRoot" +Write-Host "CLI project: $cliProject" + +Invoke-RepoRestore + +& dotnet build $cliProject /p:SkipNativeBuild=true + +$appHosts = Get-ValidationAppHosts +Write-Host "Found $($appHosts.Count) TypeScript playground apps matching '$AppPattern'." + +$failures = [System.Collections.Generic.List[string]]::new() +$updated = [System.Collections.Generic.List[string]]::new() + +foreach ($appHost in $appHosts) { + $appName = '{0}/{1}' -f (Split-Path -Path $appHost.FullName -Parent | Split-Path -Leaf), $appHost.Name + + Write-Host '' + Write-Host '----------------------------------------' + Write-Host "Refreshing: $appName" + Write-Host '----------------------------------------' + + Push-Location $appHost.FullName + try { + Write-Host ' -> Installing npm dependencies...' + Install-NodeDependencies -appDir $appHost.FullName + + $generatedDir = Join-Path $appHost.FullName '.modules' + if (Test-Path $generatedDir) { + Write-Host ' -> Clearing existing generated SDK...' + Remove-Item -Path $generatedDir -Recurse -Force + } + + Write-Host ' -> Running aspire restore...' + & dotnet run --no-build --project $cliProject -- restore + + Write-Host ' -> Verifying generated SDK...' + Assert-GeneratedSdkFiles -appDir $appHost.FullName + + Write-Host " OK $appName refreshed" + $updated.Add($appName) + } + catch { + Write-Host " ERROR failed to refresh $appName" + Write-Host ($_ | Out-String) + $failures.Add($appName) + } + finally { + Pop-Location + } +} + +Write-Host '' +Write-Host '----------------------------------------' +Write-Host "Results: $($updated.Count) refreshed, $($failures.Count) failed out of $($appHosts.Count) apps" +Write-Host '----------------------------------------' + +if ($failures.Count -gt 0) { + Write-Host '' + Write-Host 'Failed apps:' + foreach ($failure in $failures) { + Write-Host " - $failure" + } + + exit 1 +} + +Write-Host 'All TypeScript playground SDKs refreshed successfully.' diff --git a/eng/testing/github-ci-trigger-patterns.txt b/eng/testing/github-ci-trigger-patterns.txt index 0287dc81ab7..28df2daa936 100644 --- a/eng/testing/github-ci-trigger-patterns.txt +++ b/eng/testing/github-ci-trigger-patterns.txt @@ -26,6 +26,8 @@ eng/testing/github-ci-trigger-patterns.txt # Engineering pipeline scripts (Azure DevOps, not used in the GitHub CI build) eng/pipelines/** +eng/homebrew/** +eng/winget/** eng/test-configuration.json Aspire-Core.slnf diff --git a/eng/winget/README.md b/eng/winget/README.md index af32e1ad807..d42547f9af3 100644 --- a/eng/winget/README.md +++ b/eng/winget/README.md @@ -41,7 +41,7 @@ Windows only (x64, arm64). Installers are zip archives containing a portable `as ## Artifact URLs ```text -https://ci.dot.net/public/aspire/{VERSION}/aspire-cli-win-{arch}-{VERSION}.zip +https://ci.dot.net/public/aspire/{ARTIFACT_VERSION}/aspire-cli-win-{arch}-{VERSION}.zip ``` Where arch is `x64` or `arm64`. diff --git a/eng/winget/generate-manifests.ps1 b/eng/winget/generate-manifests.ps1 index d583809138b..c97ea2d5ae9 100644 --- a/eng/winget/generate-manifests.ps1 +++ b/eng/winget/generate-manifests.ps1 @@ -5,10 +5,13 @@ .DESCRIPTION This script generates the required WinGet manifest files (version, locale, and installer) from templates by substituting version numbers, URLs, and computing SHA256 hashes. - Installer URLs are derived from the version and RIDs using the ci.dot.net URL pattern. + Installer URLs are derived from the installer version, artifact version, and RIDs using the ci.dot.net URL pattern. .PARAMETER Version - The version number for the package (e.g., "13.3.0-preview.1.26111.5"). + The package version and installer filename version (e.g., "13.2.0"). + +.PARAMETER ArtifactVersion + The version segment used in the ci.dot.net artifact path. Defaults to Version. .PARAMETER TemplateDir The directory containing the manifest templates to use. @@ -24,6 +27,10 @@ e.g., "./manifests/m/Microsoft/Aspire/{Version}" for Microsoft.Aspire or "./manifests/m/Microsoft/Aspire/Prerelease/{Version}" for Microsoft.Aspire.Prerelease. +.PARAMETER ArchiveRoot + Root directory containing locally built CLI archives. When specified, SHA256 hashes + are computed from matching local files instead of downloading from installer URLs. + .PARAMETER ReleaseNotesUrl URL to the release notes page. If not specified, derived from the version (e.g., "13.2.0" -> "https://aspire.dev/whats-new/aspire-13-2/"). @@ -38,6 +45,7 @@ .EXAMPLE ./generate-manifests.ps1 -Version "13.2.0" ` + -ArtifactVersion "13.2.0-preview.1.26111.5" ` -TemplateDir "./eng/winget/microsoft.aspire" ` -Rids "win-x64,win-arm64" -ValidateUrls #> @@ -47,6 +55,9 @@ param( [Parameter(Mandatory = $true)] [string]$Version, + [Parameter(Mandatory = $false)] + [string]$ArtifactVersion, + [Parameter(Mandatory = $true)] [string]$TemplateDir, @@ -56,6 +67,9 @@ param( [Parameter(Mandatory = $false)] [string]$OutputPath, + [Parameter(Mandatory = $false)] + [string]$ArchiveRoot, + [Parameter(Mandatory = $false)] [string]$ReleaseNotesUrl, @@ -65,6 +79,10 @@ param( $ErrorActionPreference = 'Stop' +if ([string]::IsNullOrWhiteSpace($ArtifactVersion)) { + $ArtifactVersion = $Version +} + # Determine script paths $ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path @@ -74,6 +92,11 @@ if (-not (Test-Path $TemplateDir)) { exit 1 } +if ($ArchiveRoot -and -not (Test-Path $ArchiveRoot)) { + Write-Error "Archive root directory not found: $ArchiveRoot" + exit 1 +} + # Extract PackageIdentifier from the version template $versionTemplatePath = Join-Path $TemplateDir "Aspire.yaml.template" if (-not (Test-Path $versionTemplatePath)) { @@ -133,14 +156,15 @@ function Get-ArchitectureFromRid { } # Build installer URL from version and RID -# Pattern: https://ci.dot.net/public/aspire/{version}/aspire-cli-{rid}-{version}.zip +# Pattern: https://ci.dot.net/public/aspire/{artifactVersion}/aspire-cli-{rid}-{version}.zip function Get-InstallerUrl { param( [string]$Version, + [string]$ArtifactVersion, [string]$Rid ) - return "https://ci.dot.net/public/aspire/$Version/aspire-cli-$Rid-$Version.zip" + return "https://ci.dot.net/public/aspire/$ArtifactVersion/aspire-cli-$Rid-$Version.zip" } # Function to compute SHA256 hash of a file downloaded from URL @@ -169,6 +193,43 @@ function Get-RemoteFileSha256 { } } +function Get-LocalArchivePath { + param( + [string]$ArchiveRoot, + [string]$Rid, + [string]$Version + ) + + $archiveName = "aspire-cli-$Rid-$Version.zip" + $matches = @(Get-ChildItem -Path $ArchiveRoot -File -Recurse -Filter $archiveName | Sort-Object FullName) + if ($matches.Count -eq 0) { + Write-Error "Could not find local archive '$archiveName' under '$ArchiveRoot'" + exit 1 + } + + if ($matches.Count -gt 1) { + $matchList = $matches | ForEach-Object { " $($_.FullName)" } + Write-Error "Found multiple local archives named '$archiveName' under '$ArchiveRoot':`n$($matchList -join "`n")" + exit 1 + } + + return $matches[0].FullName +} + +function Get-LocalFileSha256 { + param( + [string]$Path, + [string]$Description + ) + + Write-Host "Computing SHA256 for $Description from local file..." + Write-Host " Path: $Path" + + $hash = (Get-FileHash -Path $Path -Algorithm SHA256).Hash.ToUpperInvariant() + Write-Host " SHA256: $hash" + return $hash +} + # Function to process a template file function Process-Template { param( @@ -199,7 +260,7 @@ Write-Host "" $installerEntries = @() foreach ($rid in $ridList) { $arch = Get-ArchitectureFromRid -Rid $rid - $url = Get-InstallerUrl -Version $Version -Rid $rid + $url = Get-InstallerUrl -Version $Version -ArtifactVersion $ArtifactVersion -Rid $rid $installerEntries += @{ Rid = $rid; Architecture = $arch; Url = $url } } @@ -230,7 +291,13 @@ Write-Host "Computing SHA256 hashes..." $installersYaml = "Installers:" foreach ($entry in $installerEntries) { - $sha256 = Get-RemoteFileSha256 -Url $entry.Url -Description "$($entry.Rid) installer" + if ($ArchiveRoot) { + $archivePath = Get-LocalArchivePath -ArchiveRoot $ArchiveRoot -Rid $entry.Rid -Version $Version + $sha256 = Get-LocalFileSha256 -Path $archivePath -Description "$($entry.Rid) installer" + } + else { + $sha256 = Get-RemoteFileSha256 -Url $entry.Url -Description "$($entry.Rid) installer" + } $installersYaml += "`n- Architecture: $($entry.Architecture)" $installersYaml += "`n InstallerUrl: $($entry.Url)" @@ -288,6 +355,6 @@ Write-Host "Successfully generated WinGet manifests at: $OutputPath" Write-Host "" Write-Host "Next steps:" Write-Host " 1. Validate manifests: winget validate --manifest `"$OutputPath`"" -Write-Host " 2. Submit to winget-pkgs: wingetcreate submit --token YOUR_PAT `"$OutputPath`"" +Write-Host " 2. Submit to winget-pkgs: wingetcreate submit --token YOUR_PAT `"$([System.IO.Path]::Combine($OutputPath, "$PackageIdentifier.yaml"))`" `"$([System.IO.Path]::Combine($OutputPath, "$PackageIdentifier.installer.yaml"))`" `"$([System.IO.Path]::Combine($OutputPath, "$PackageIdentifier.locale.en-US.yaml"))`"" exit 0 diff --git a/eng/winget/microsoft.aspire.prerelease/Aspire.locale.en-US.yaml.template b/eng/winget/microsoft.aspire.prerelease/Aspire.locale.en-US.yaml.template index c000e010071..3655c7d5ed2 100644 --- a/eng/winget/microsoft.aspire.prerelease/Aspire.locale.en-US.yaml.template +++ b/eng/winget/microsoft.aspire.prerelease/Aspire.locale.en-US.yaml.template @@ -5,12 +5,12 @@ PackageVersion: "${VERSION}" PackageLocale: en-US Publisher: Microsoft Corporation PublisherUrl: https://aspire.dev/ -PublisherSupportUrl: https://github.com/dotnet/aspire/issues +PublisherSupportUrl: https://github.com/microsoft/aspire/issues PrivacyUrl: https://privacy.microsoft.com/privacystatement PackageName: Aspire CLI (Prerelease) PackageUrl: https://aspire.dev/ License: MIT -LicenseUrl: https://github.com/dotnet/aspire/blob/main/LICENSE.TXT +LicenseUrl: https://github.com/microsoft/aspire/blob/main/LICENSE.TXT Copyright: (c) Microsoft ${YEAR} ShortDescription: Prerelease CLI tool for building observable, production-ready distributed applications with Aspire Description: > diff --git a/eng/winget/microsoft.aspire/Aspire.locale.en-US.yaml.template b/eng/winget/microsoft.aspire/Aspire.locale.en-US.yaml.template index e020b5ccccc..ad4ae1c06e9 100644 --- a/eng/winget/microsoft.aspire/Aspire.locale.en-US.yaml.template +++ b/eng/winget/microsoft.aspire/Aspire.locale.en-US.yaml.template @@ -5,13 +5,13 @@ PackageVersion: "${VERSION}" PackageLocale: en-US Publisher: Microsoft Corporation PublisherUrl: https://aspire.dev/ -PublisherSupportUrl: https://github.com/dotnet/aspire/issues +PublisherSupportUrl: https://github.com/microsoft/aspire/issues PrivacyUrl: https://privacy.microsoft.com/privacystatement Moniker: aspire PackageName: Aspire CLI PackageUrl: https://aspire.dev/ License: MIT -LicenseUrl: https://github.com/dotnet/aspire/blob/main/LICENSE.TXT +LicenseUrl: https://github.com/microsoft/aspire/blob/main/LICENSE.TXT Copyright: (c) Microsoft ${YEAR} ShortDescription: CLI tool for building observable, production-ready distributed applications with Aspire Description: > diff --git a/playground/FoundryAgentEnterprise/frontend/src/App.tsx b/playground/FoundryAgentEnterprise/frontend/src/App.tsx index fb1a47b6150..66f1678f694 100644 --- a/playground/FoundryAgentEnterprise/frontend/src/App.tsx +++ b/playground/FoundryAgentEnterprise/frontend/src/App.tsx @@ -166,7 +166,7 @@ function App() { Learn more about Aspire (opens in new tab) - © @DateTime.Today.Year @@ -14,7 +14,7 @@ @Message - diff --git a/playground/TypeScriptApps/RpsArena/aspire.config.json b/playground/TypeScriptApps/RpsArena/aspire.config.json index 1bd1eebff0e..92404cb5c63 100644 --- a/playground/TypeScriptApps/RpsArena/aspire.config.json +++ b/playground/TypeScriptApps/RpsArena/aspire.config.json @@ -10,5 +10,14 @@ "Aspire.Hosting.Python": "13.3.0-preview.1.26167.8", "Aspire.Hosting.PostgreSQL": "13.3.0-preview.1.26167.8", "Aspire.Hosting.JavaScript": "13.3.0-preview.1.26167.8" + }, + "profiles": { + "https": { + "applicationUrl": "https://rpsarena.dev.localhost:16323;http://rpsarena.dev.localhost:15163", + "environmentVariables": { + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://rpsarena.dev.localhost:35807", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://rpsarena.dev.localhost:32833" + } + } } } diff --git a/playground/polyglot/TypeScript/Aspire.Hosting.SqlServer/aspire.config.json b/playground/polyglot/TypeScript/Aspire.Hosting.SqlServer/aspire.config.json index d103fa29559..280871d8613 100644 --- a/playground/polyglot/TypeScript/Aspire.Hosting.SqlServer/aspire.config.json +++ b/playground/polyglot/TypeScript/Aspire.Hosting.SqlServer/aspire.config.json @@ -3,7 +3,18 @@ "path": "ValidationAppHost/apphost.ts", "language": "typescript/nodejs" }, + "profiles": { + "https": { + "applicationUrl": "https://localhost:55686;http://localhost:57768", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_ENVIRONMENT": "Development", + "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:51980", + "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:28684" + } + } + }, "packages": { "Aspire.Hosting.SqlServer": "" } -} +} \ No newline at end of file diff --git a/playground/polyglot/TypeScript/Aspire.Hosting/ValidationAppHost/apphost.ts b/playground/polyglot/TypeScript/Aspire.Hosting/ValidationAppHost/apphost.ts index 3f2cd131d3b..fabecdca04a 100644 --- a/playground/polyglot/TypeScript/Aspire.Hosting/ValidationAppHost/apphost.ts +++ b/playground/polyglot/TypeScript/Aspire.Hosting/ValidationAppHost/apphost.ts @@ -69,18 +69,23 @@ const builtConnectionString = await builder.addConnectionStringBuilder("customcs await builtConnectionString.withConnectionProperty("Host", expr); await builtConnectionString.withConnectionPropertyValue("Mode", "Development"); +const envConnectionString = await builder.addConnectionString("envcs"); + // =================================================================== // ResourceBuilderExtensions.cs — NEW exports on ContainerResource // =================================================================== -// withEnvironmentEndpoint -await container.withEnvironmentEndpoint("MY_ENDPOINT", endpoint); +// withEnvironment — with EndpointReference +await container.withEnvironment("MY_ENDPOINT", endpoint); + +// withEnvironment — with ReferenceExpression +await container.withEnvironment("MY_EXPR", expr); -// withEnvironmentParameter -await container.withEnvironmentParameter("MY_PARAM", configParam); +// withEnvironment — with ParameterResource +await container.withEnvironment("MY_PARAM", configParam); -// withEnvironmentConnectionString -await container.withEnvironmentConnectionString("MY_CONN", builtConnectionString); +// withEnvironment — with connection string resource +await container.withEnvironment("MY_CONN", envConnectionString); // withConnectionProperty — with ReferenceExpression await builtConnectionString.withConnectionProperty("Endpoint", expr); diff --git a/src/Aspire.Cli/Aspire.Cli.csproj b/src/Aspire.Cli/Aspire.Cli.csproj index 70d5415a57d..4a3e79fd5bf 100644 --- a/src/Aspire.Cli/Aspire.Cli.csproj +++ b/src/Aspire.Cli/Aspire.Cli.csproj @@ -58,6 +58,7 @@ + @@ -78,6 +79,7 @@ + diff --git a/src/Aspire.Cli/CliExecutionContext.cs b/src/Aspire.Cli/CliExecutionContext.cs index 062c2b6c35c..59c31c93a89 100644 --- a/src/Aspire.Cli/CliExecutionContext.cs +++ b/src/Aspire.Cli/CliExecutionContext.cs @@ -5,13 +5,18 @@ namespace Aspire.Cli; -internal sealed class CliExecutionContext(DirectoryInfo workingDirectory, DirectoryInfo hivesDirectory, DirectoryInfo cacheDirectory, DirectoryInfo sdksDirectory, DirectoryInfo logsDirectory, string logFilePath, bool debugMode = false, IReadOnlyDictionary? environmentVariables = null, DirectoryInfo? homeDirectory = null) +internal sealed class CliExecutionContext(DirectoryInfo workingDirectory, DirectoryInfo hivesDirectory, DirectoryInfo cacheDirectory, DirectoryInfo sdksDirectory, DirectoryInfo logsDirectory, string logFilePath, bool debugMode = false, IReadOnlyDictionary? environmentVariables = null, DirectoryInfo? homeDirectory = null, DirectoryInfo? packagesDirectory = null) { public DirectoryInfo WorkingDirectory { get; } = workingDirectory; public DirectoryInfo HivesDirectory { get; } = hivesDirectory; public DirectoryInfo CacheDirectory { get; } = cacheDirectory; public DirectoryInfo SdksDirectory { get; } = sdksDirectory; + /// + /// Gets the directory where restored NuGet packages are cached for apphost server sessions. + /// + public DirectoryInfo? PackagesDirectory { get; } = packagesDirectory; + /// /// Gets the directory where CLI log files are stored. /// Used by cache clear command to clean up old log files. diff --git a/src/Aspire.Cli/Commands/CacheCommand.cs b/src/Aspire.Cli/Commands/CacheCommand.cs index 860d29e1332..1fb343610bf 100644 --- a/src/Aspire.Cli/Commands/CacheCommand.cs +++ b/src/Aspire.Cli/Commands/CacheCommand.cs @@ -32,7 +32,7 @@ protected override Task ExecuteAsync(ParseResult parseResult, CancellationT return Task.FromResult(ExitCodeConstants.InvalidCommand); } - private sealed class ClearCommand : BaseCommand + internal sealed class ClearCommand : BaseCommand { public ClearCommand(IInteractionService interactionService, IFeatures features, ICliUpdateNotifier updateNotifier, CliExecutionContext executionContext, AspireCliTelemetry telemetry) : base("clear", CacheCommandStrings.ClearCommand_Description, features, updateNotifier, executionContext, interactionService, telemetry) @@ -45,108 +45,15 @@ protected override Task ExecuteAsync(ParseResult parseResult, CancellationT { try { - var cacheDirectory = ExecutionContext.CacheDirectory; var filesDeleted = 0; - - // Delete cache files and subdirectories - if (cacheDirectory.Exists) - { - // Delete all cache files and subdirectories - foreach (var file in cacheDirectory.GetFiles("*", SearchOption.AllDirectories)) - { - try - { - file.Delete(); - filesDeleted++; - } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) - { - // Continue deleting other files even if some fail - } - } - - // Delete subdirectories - foreach (var directory in cacheDirectory.GetDirectories()) - { - try - { - directory.Delete(recursive: true); - } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) - { - // Continue deleting other directories even if some fail - } - } - } - - // Also clear the sdks directory - var sdksDirectory = ExecutionContext.SdksDirectory; - if (sdksDirectory.Exists) - { - foreach (var file in sdksDirectory.GetFiles("*", SearchOption.AllDirectories)) - { - try - { - file.Delete(); - filesDeleted++; - } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) - { - // Continue deleting other files even if some fail - } - } - - // Delete subdirectories - foreach (var directory in sdksDirectory.GetDirectories()) - { - try - { - directory.Delete(recursive: true); - } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) - { - // Continue deleting other directories even if some fail - } - } - } - - // Also clear the logs directory (skip current process's log file) - var logsDirectory = ExecutionContext.LogsDirectory; var currentLogFilePath = ExecutionContext.LogFilePath; - if (logsDirectory.Exists) - { - foreach (var file in logsDirectory.GetFiles("*", SearchOption.AllDirectories)) - { - // Skip the current process's log file to avoid deleting it while in use - if (file.FullName.Equals(currentLogFilePath, StringComparison.OrdinalIgnoreCase)) - { - continue; - } - - try - { - file.Delete(); - filesDeleted++; - } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) - { - // Continue deleting other files even if some fail - } - } - - // Delete subdirectories - foreach (var directory in logsDirectory.GetDirectories()) - { - try - { - directory.Delete(recursive: true); - } - catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) - { - // Continue deleting other directories even if some fail - } - } - } + + filesDeleted += ClearDirectoryContents(ExecutionContext.CacheDirectory); + filesDeleted += ClearDirectoryContents(ExecutionContext.SdksDirectory); + filesDeleted += ClearDirectoryContents(ExecutionContext.PackagesDirectory); + filesDeleted += ClearDirectoryContents( + ExecutionContext.LogsDirectory, + skipFile: f => f.FullName.Equals(currentLogFilePath, StringComparison.OrdinalIgnoreCase)); if (filesDeleted == 0) { @@ -167,5 +74,53 @@ protected override Task ExecuteAsync(ParseResult parseResult, CancellationT return Task.FromResult(ExitCodeConstants.InvalidCommand); } } + + private static readonly EnumerationOptions s_enumerationOptions = new() + { + RecurseSubdirectories = true, + IgnoreInaccessible = true + }; + + internal static int ClearDirectoryContents(DirectoryInfo? directory, Func? skipFile = null) + { + if (directory is null || !directory.Exists) + { + return 0; + } + + var filesDeleted = 0; + + foreach (var file in directory.EnumerateFiles("*", s_enumerationOptions)) + { + if (skipFile?.Invoke(file) == true) + { + continue; + } + + try + { + file.Delete(); + filesDeleted++; + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) + { + // Continue deleting other files even if some fail (e.g. locked by a running process) + } + } + + foreach (var subdirectory in directory.EnumerateDirectories()) + { + try + { + subdirectory.Delete(recursive: true); + } + catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or System.Security.SecurityException) + { + // Continue deleting other directories even if some fail + } + } + + return filesDeleted; + } } } diff --git a/src/Aspire.Cli/Commands/InitCommand.cs b/src/Aspire.Cli/Commands/InitCommand.cs index b83f3814977..02fbe625c4d 100644 --- a/src/Aspire.Cli/Commands/InitCommand.cs +++ b/src/Aspire.Cli/Commands/InitCommand.cs @@ -149,6 +149,12 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell InteractionService.DisplayMessage(KnownEmojis.Information, $"Creating {languageInfo.DisplayName} AppHost..."); InteractionService.DisplayEmptyLine(); var polyglotResult = await CreatePolyglotAppHostAsync(languageInfo, cancellationToken); + if (polyglotResult != 0) + { + InteractionService.DisplayError(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ProjectCouldNotBeCreated, ExecutionContext.LogFilePath)); + return polyglotResult; + } + return await _agentInitCommand.PromptAndChainAsync(_hostEnvironment, InteractionService, polyglotResult, _executionContext.WorkingDirectory, cancellationToken); } @@ -183,6 +189,12 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell workspaceRoot = _executionContext.WorkingDirectory; } + if (initResult != 0) + { + InteractionService.DisplayError(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ProjectCouldNotBeCreated, ExecutionContext.LogFilePath)); + return initResult; + } + return await _agentInitCommand.PromptAndChainAsync(_hostEnvironment, InteractionService, initResult, workspaceRoot, cancellationToken); } diff --git a/src/Aspire.Cli/Commands/NewCommand.cs b/src/Aspire.Cli/Commands/NewCommand.cs index ca844097b24..aec90dc7182 100644 --- a/src/Aspire.Cli/Commands/NewCommand.cs +++ b/src/Aspire.Cli/Commands/NewCommand.cs @@ -3,6 +3,7 @@ using System.CommandLine; using System.Diagnostics.CodeAnalysis; +using System.Globalization; using System.Text.RegularExpressions; using Aspire.Cli.Configuration; using Aspire.Cli.Interaction; @@ -255,6 +256,7 @@ protected override async Task ExecuteAsync(ParseResult parseResult, Cancell if (!resolveResult.Success) { InteractionService.DisplayError(resolveResult.ErrorMessage); + InteractionService.DisplayError(string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ProjectCouldNotBeCreated, ExecutionContext.LogFilePath)); return ExitCodeConstants.InvalidCommand; } diff --git a/src/Aspire.Cli/Commands/Sdk/SdkDumpCommand.cs b/src/Aspire.Cli/Commands/Sdk/SdkDumpCommand.cs index 1be71d007af..bf97051d992 100644 --- a/src/Aspire.Cli/Commands/Sdk/SdkDumpCommand.cs +++ b/src/Aspire.Cli/Commands/Sdk/SdkDumpCommand.cs @@ -164,84 +164,67 @@ private async Task DumpCapabilitiesAsync( return ExitCodeConstants.FailedToBuildArtifacts; } - // Start the server - var currentPid = Environment.ProcessId; - var (socketPath, serverProcess, _) = appHostServerProject.Run(currentPid, new Dictionary()); + await using var serverSession = AppHostServerSession.Start( + appHostServerProject, + environmentVariables: null, + debug: false, + _logger); - try - { - // Connect and get capabilities - await using var rpcClient = await AppHostRpcClient.ConnectAsync(socketPath, cancellationToken); + // Connect and get capabilities + var rpcClient = await serverSession.GetRpcClientAsync(cancellationToken); - _logger.LogDebug("Fetching capabilities via RPC"); - var capabilities = await rpcClient.GetCapabilitiesAsync(cancellationToken); + _logger.LogDebug("Fetching capabilities via RPC"); + var capabilities = await rpcClient.GetCapabilitiesAsync(cancellationToken); - // Output Info-level diagnostics to stderr via logger (shown with -d flag) - var infoDiagnostics = capabilities.Diagnostics.Where(d => d.Severity == "Info").ToList(); - foreach (var diag in infoDiagnostics) - { - var location = string.IsNullOrEmpty(diag.Location) ? "" : $" [{diag.Location}]"; - _logger.LogDebug("{Message}{Location}", diag.Message, location); - } + // Output Info-level diagnostics to stderr via logger (shown with -d flag) + var infoDiagnostics = capabilities.Diagnostics.Where(d => d.Severity == "Info").ToList(); + foreach (var diag in infoDiagnostics) + { + var location = string.IsNullOrEmpty(diag.Location) ? "" : $" [{diag.Location}]"; + _logger.LogDebug("{Message}{Location}", diag.Message, location); + } - // Remove Info diagnostics from output (they go to stderr only) - capabilities.Diagnostics.RemoveAll(d => d.Severity == "Info"); + // Remove Info diagnostics from output (they go to stderr only) + capabilities.Diagnostics.RemoveAll(d => d.Severity == "Info"); - // Stamp package versions for integrations that have them - var packageVersions = integrations - .Where(i => i.IsPackageReference) - .Select(i => new PackageInfo { Name = i.Name, Version = i.Version! }) - .ToList(); - if (packageVersions.Count > 0) - { - capabilities.Packages = packageVersions; - } + // Stamp package versions for integrations that have them + var packageVersions = integrations + .Where(i => i.IsPackageReference) + .Select(i => new PackageInfo { Name = i.Name, Version = i.Version! }) + .ToList(); + if (packageVersions.Count > 0) + { + capabilities.Packages = packageVersions; + } - // Format the output - var output = format switch - { - OutputFormat.Json => FormatJson(capabilities), - OutputFormat.Ci => FormatCi(capabilities), - _ => FormatPretty(capabilities) - }; + // Format the output + var output = format switch + { + OutputFormat.Json => FormatJson(capabilities), + OutputFormat.Ci => FormatCi(capabilities), + _ => FormatPretty(capabilities) + }; - // Write output - if (outputFile is not null) - { - var outputDir = outputFile.Directory; - if (outputDir is not null && !outputDir.Exists) - { - outputDir.Create(); - } - await File.WriteAllTextAsync(outputFile.FullName, output, cancellationToken); - InteractionService.DisplaySuccess($"Capabilities written to {outputFile.FullName}"); - } - else + // Write output + if (outputFile is not null) + { + var outputDir = outputFile.Directory; + if (outputDir is not null && !outputDir.Exists) { - // Output to stdout - InteractionService.DisplayRawText(output, consoleOverride: ConsoleOutput.Standard); + outputDir.Create(); } - - // Return error code if there are errors in diagnostics - var hasErrors = capabilities.Diagnostics.Exists(d => d.Severity == "Error"); - return hasErrors ? ExitCodeConstants.InvalidCommand : ExitCodeConstants.Success; + await File.WriteAllTextAsync(outputFile.FullName, output, cancellationToken); + InteractionService.DisplaySuccess($"Capabilities written to {outputFile.FullName}"); } - finally + else { - // Stop the server - just try to kill, catch if already exited - try - { - serverProcess.Kill(entireProcessTree: true); - } - catch (InvalidOperationException) - { - // Process already exited - this is fine - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Error killing AppHost server process"); - } + // Output to stdout + InteractionService.DisplayRawText(output, consoleOverride: ConsoleOutput.Standard); } + + // Return error code if there are errors in diagnostics + var hasErrors = capabilities.Diagnostics.Exists(d => d.Severity == "Error"); + return hasErrors ? ExitCodeConstants.InvalidCommand : ExitCodeConstants.Success; } finally { diff --git a/src/Aspire.Cli/Commands/Sdk/SdkGenerateCommand.cs b/src/Aspire.Cli/Commands/Sdk/SdkGenerateCommand.cs index 5935fdc9561..ec0ab2021e2 100644 --- a/src/Aspire.Cli/Commands/Sdk/SdkGenerateCommand.cs +++ b/src/Aspire.Cli/Commands/Sdk/SdkGenerateCommand.cs @@ -158,60 +158,43 @@ private async Task GenerateSdkAsync( return ExitCodeConstants.FailedToBuildArtifacts; } - // Start the server - var currentPid = Environment.ProcessId; - var (socketPath, serverProcess, _) = appHostServerProject.Run(currentPid, new Dictionary()); + await using var serverSession = AppHostServerSession.Start( + appHostServerProject, + environmentVariables: null, + debug: false, + _logger); - try - { - // Connect and generate code - await using var rpcClient = await AppHostRpcClient.ConnectAsync(socketPath, cancellationToken); - - _logger.LogDebug("Generating {Language} SDK via RPC", languageInfo.CodeGenerator); - var generatedFiles = await rpcClient.GenerateCodeAsync(languageInfo.CodeGenerator, cancellationToken); - - // Write generated files - var outputDirFullPath = Path.GetFullPath(outputDir.FullName); - foreach (var (fileName, content) in generatedFiles) - { - var filePath = Path.GetFullPath(Path.Combine(outputDir.FullName, fileName)); + // Connect and generate code + var rpcClient = await serverSession.GetRpcClientAsync(cancellationToken); - // Validate path is within output directory (prevent path traversal) - if (!filePath.StartsWith(outputDirFullPath, StringComparison.OrdinalIgnoreCase)) - { - _logger.LogWarning("Skipping file with invalid path: {FileName}", fileName); - continue; - } + _logger.LogDebug("Generating {Language} SDK via RPC", languageInfo.CodeGenerator); + var generatedFiles = await rpcClient.GenerateCodeAsync(languageInfo.CodeGenerator, cancellationToken); - var fileDirectory = Path.GetDirectoryName(filePath); - if (!string.IsNullOrEmpty(fileDirectory)) - { - Directory.CreateDirectory(fileDirectory); - } - await File.WriteAllTextAsync(filePath, content, cancellationToken); - _logger.LogDebug("Wrote {FileName}", fileName); - } - - InteractionService.DisplaySuccess($"Generated {generatedFiles.Count} files in {outputDir.FullName}"); - - return ExitCodeConstants.Success; - } - finally + // Write generated files + var outputDirFullPath = Path.GetFullPath(outputDir.FullName); + foreach (var (fileName, content) in generatedFiles) { - // Stop the server - just try to kill, catch if already exited - try - { - serverProcess.Kill(entireProcessTree: true); - } - catch (InvalidOperationException) + var filePath = Path.GetFullPath(Path.Combine(outputDir.FullName, fileName)); + + // Validate path is within output directory (prevent path traversal) + if (!filePath.StartsWith(outputDirFullPath, StringComparison.OrdinalIgnoreCase)) { - // Process already exited - this is fine + _logger.LogWarning("Skipping file with invalid path: {FileName}", fileName); + continue; } - catch (Exception ex) + + var fileDirectory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(fileDirectory)) { - _logger.LogDebug(ex, "Error killing AppHost server process"); + Directory.CreateDirectory(fileDirectory); } + await File.WriteAllTextAsync(filePath, content, cancellationToken); + _logger.LogDebug("Wrote {FileName}", fileName); } + + InteractionService.DisplaySuccess($"Generated {generatedFiles.Count} files in {outputDir.FullName}"); + + return ExitCodeConstants.Success; } finally { diff --git a/src/Aspire.Cli/Configuration/AspireConfigFile.cs b/src/Aspire.Cli/Configuration/AspireConfigFile.cs index a535b419128..fb909e01437 100644 --- a/src/Aspire.Cli/Configuration/AspireConfigFile.cs +++ b/src/Aspire.Cli/Configuration/AspireConfigFile.cs @@ -7,6 +7,7 @@ using System.Text.Json.Serialization; using Aspire.Cli.Resources; using Aspire.Cli.Utils; +using Aspire.Hosting.Utils; using Microsoft.Extensions.Logging; namespace Aspire.Cli.Configuration; @@ -174,6 +175,20 @@ public static AspireConfigFile LoadOrCreate(string directory, string? defaultSdk var profiles = ReadApphostRunProfiles(Path.Combine(directory, "apphost.run.json")); config = FromLegacy(legacyConfig, profiles); + // Legacy .aspire/settings.json stores appHostPath relative to the .aspire/ directory, + // but aspire.config.json stores it relative to the config file's own directory (the parent + // of .aspire/). Re-base the path so it resolves correctly from the new location. + // Paths are always stored with '/' separators regardless of platform, but we normalize + // to the OS separator for Path operations and back to '/' for storage. + if (config.AppHost?.Path is { Length: > 0 } migratedPath && !Path.IsPathRooted(migratedPath)) + { + var legacySettingsDir = Path.Combine(directory, AspireJsonConfiguration.SettingsFolder); + var absolutePath = PathNormalizer.NormalizePathForCurrentPlatform( + Path.Combine(legacySettingsDir, migratedPath)); + config.AppHost.Path = PathNormalizer.NormalizePathForStorage( + Path.GetRelativePath(directory, absolutePath)); + } + // Persist the migrated config (legacy files are kept for older CLI versions) config.Save(directory); } diff --git a/src/Aspire.Cli/Configuration/FlexibleBooleanConverter.cs b/src/Aspire.Cli/Configuration/FlexibleBooleanConverter.cs new file mode 100644 index 00000000000..5e75f0d04d3 --- /dev/null +++ b/src/Aspire.Cli/Configuration/FlexibleBooleanConverter.cs @@ -0,0 +1,42 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Aspire.Cli.Configuration; + +/// +/// A JSON converter for that accepts both actual boolean values +/// (true/false) and string representations ("true"/"false"). +/// This provides backward compatibility for settings files that may have string values +/// written by older CLI versions. +/// +internal sealed class FlexibleBooleanConverter : JsonConverter +{ + public override bool Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + return reader.TokenType switch + { + JsonTokenType.True => true, + JsonTokenType.False => false, + JsonTokenType.String => ParseString(reader.GetString()), + _ => throw new JsonException($"Unexpected token parsing boolean. Token: {reader.TokenType}") + }; + } + + private static bool ParseString(string? value) + { + if (bool.TryParse(value, out var result)) + { + return result; + } + + throw new JsonException($"Invalid boolean value: '{value}'. Expected 'true' or 'false'."); + } + + public override void Write(Utf8JsonWriter writer, bool value, JsonSerializerOptions options) + { + writer.WriteBooleanValue(value); + } +} diff --git a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs index 2f88d686563..8ac6505d4ca 100644 --- a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs @@ -83,17 +83,7 @@ private string GetMsBuildServerValue() internal static string GetBackchannelSocketPath() { - var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var aspireCliPath = Path.Combine(homeDirectory, ".aspire", "cli", "backchannels"); - - if (!Directory.Exists(aspireCliPath)) - { - Directory.CreateDirectory(aspireCliPath); - } - - var uniqueSocketPathSegment = Guid.NewGuid().ToString("N"); - var socketPath = Path.Combine(aspireCliPath, $"cli.sock.{uniqueSocketPathSegment}"); - return socketPath; + return CliPathHelper.CreateUnixDomainSocketPath("cli.sock"); } private async Task ExecuteAsync( diff --git a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs index ba15c1af916..db81e784f11 100644 --- a/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs +++ b/src/Aspire.Cli/Interaction/ConsoleInteractionService.cs @@ -352,15 +352,24 @@ public void DisplaySuccess(string message, bool allowMarkup = false) public void DisplayLines(IEnumerable<(OutputLineStream Stream, string Line)> lines) { - foreach (var (stream, line) in lines) + var linesArray = lines.ToArray(); + + // Special case one stderr line to include error icon. + if (linesArray.Length == 1 && linesArray[0].Stream == OutputLineStream.StdErr) + { + DisplayError(linesArray[0].Line); + return; + } + + foreach (var (stream, line) in linesArray) { if (stream == OutputLineStream.StdOut) { - MessageConsole.MarkupLineInterpolated($"{line.EscapeMarkup()}"); + MessageConsole.MarkupLine(line.EscapeMarkup()); } else { - MessageConsole.MarkupLineInterpolated($"[red]{line.EscapeMarkup()}[/]"); + MessageConsole.MarkupLine($"[red]{line.EscapeMarkup()}[/]"); } } } diff --git a/src/Aspire.Cli/JsonSourceGenerationContext.cs b/src/Aspire.Cli/JsonSourceGenerationContext.cs index ad23050e888..b0fadbcf717 100644 --- a/src/Aspire.Cli/JsonSourceGenerationContext.cs +++ b/src/Aspire.Cli/JsonSourceGenerationContext.cs @@ -18,7 +18,8 @@ namespace Aspire.Cli; WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, AllowTrailingCommas = true, - ReadCommentHandling = JsonCommentHandling.Skip)] + ReadCommentHandling = JsonCommentHandling.Skip, + Converters = [typeof(FlexibleBooleanConverter)])] [JsonSerializable(typeof(CliSettings))] [JsonSerializable(typeof(JsonObject))] [JsonSerializable(typeof(ListIntegrationsResponse))] diff --git a/src/Aspire.Cli/Packaging/PackagingService.cs b/src/Aspire.Cli/Packaging/PackagingService.cs index ddb0b6dae7a..d400d6c1298 100644 --- a/src/Aspire.Cli/Packaging/PackagingService.cs +++ b/src/Aspire.Cli/Packaging/PackagingService.cs @@ -44,7 +44,7 @@ public Task> GetChannelsAsync(CancellationToken canc // The packages subdirectory contains the actual .nupkg files // Use forward slashes for cross-platform NuGet config compatibility var packagesPath = Path.Combine(prHive.FullName, "packages").Replace('\\', '/'); - var prChannel = PackageChannel.CreateExplicitChannel(prHive.Name, PackageChannelQuality.Prerelease, new[] + var prChannel = PackageChannel.CreateExplicitChannel(prHive.Name, PackageChannelQuality.Both, new[] { new PackageMapping("Aspire*", packagesPath), new PackageMapping(PackageMapping.AllPackages, "https://api.nuget.org/v3/index.json") diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs index b7c605d425d..dc5fa35a5c9 100644 --- a/src/Aspire.Cli/Program.cs +++ b/src/Aspire.Cli/Program.cs @@ -53,9 +53,7 @@ public class Program { private static string GetUsersAspirePath() { - var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var aspirePath = Path.Combine(homeDirectory, ".aspire"); - return aspirePath; + return CliPathHelper.GetAspireHomeDirectory(); } /// @@ -517,7 +515,8 @@ private static CliExecutionContext BuildCliExecutionContext(bool debugMode, stri var hivesDirectory = GetHivesDirectory(); var cacheDirectory = GetCacheDirectory(); var sdksDirectory = GetSdksDirectory(); - return new CliExecutionContext(workingDirectory, hivesDirectory, cacheDirectory, sdksDirectory, new DirectoryInfo(logsDirectory), logFilePath, debugMode); + var packagesDirectory = GetPackagesDirectory(); + return new CliExecutionContext(workingDirectory, hivesDirectory, cacheDirectory, sdksDirectory, new DirectoryInfo(logsDirectory), logFilePath, debugMode, packagesDirectory: packagesDirectory); } private static DirectoryInfo GetCacheDirectory() @@ -527,6 +526,13 @@ private static DirectoryInfo GetCacheDirectory() return new DirectoryInfo(cacheDirectoryPath); } + private static DirectoryInfo GetPackagesDirectory() + { + var homeDirectory = GetUsersAspirePath(); + var packagesDirectoryPath = Path.Combine(homeDirectory, "packages"); + return new DirectoryInfo(packagesDirectoryPath); + } + private static void TrySetLocaleOverride(string? localeOverride, ILogger logger, IStartupErrorWriter errorWriter) { if (localeOverride is not null) diff --git a/src/Aspire.Cli/Projects/AppHostRpcClient.cs b/src/Aspire.Cli/Projects/AppHostRpcClient.cs index 74282884ce7..199bbc84371 100644 --- a/src/Aspire.Cli/Projects/AppHostRpcClient.cs +++ b/src/Aspire.Cli/Projects/AppHostRpcClient.cs @@ -24,18 +24,36 @@ private AppHostRpcClient(Stream stream, JsonRpc jsonRpc) } /// - /// Creates and connects an RPC client to the specified socket path. + /// Creates and connects an RPC client to the specified socket path and authenticates the session. /// - public static async Task ConnectAsync(string socketPath, CancellationToken cancellationToken) + public static async Task ConnectAsync(string socketPath, string authenticationToken, CancellationToken cancellationToken) { + ArgumentException.ThrowIfNullOrEmpty(authenticationToken); + var stream = await ConnectToServerAsync(socketPath, cancellationToken); + JsonRpc? jsonRpc = null; + + try + { + var formatter = BackchannelJsonSerializerContext.CreateRpcMessageFormatter(); + var handler = new HeaderDelimitedMessageHandler(stream, stream, formatter); + jsonRpc = new JsonRpc(handler); + jsonRpc.StartListening(); - var formatter = BackchannelJsonSerializerContext.CreateRpcMessageFormatter(); - var handler = new HeaderDelimitedMessageHandler(stream, stream, formatter); - var jsonRpc = new JsonRpc(handler); - jsonRpc.StartListening(); + var authenticated = await jsonRpc.InvokeWithCancellationAsync("authenticate", [authenticationToken], cancellationToken); + if (!authenticated) + { + throw new InvalidOperationException("Failed to authenticate to the AppHost server."); + } - return new AppHostRpcClient(stream, jsonRpc); + return new AppHostRpcClient(stream, jsonRpc); + } + catch + { + jsonRpc?.Dispose(); + await stream.DisposeAsync(); + throw; + } } // ═══════════════════════════════════════════════════════════════ @@ -160,8 +178,8 @@ private static async Task ConnectToServerAsync(string socketPath, Cancel internal sealed class AppHostRpcClientFactory : IAppHostRpcClientFactory { /// - public async Task ConnectAsync(string socketPath, CancellationToken cancellationToken) + public async Task ConnectAsync(string socketPath, string authenticationToken, CancellationToken cancellationToken) { - return await AppHostRpcClient.ConnectAsync(socketPath, cancellationToken); + return await AppHostRpcClient.ConnectAsync(socketPath, authenticationToken, cancellationToken); } } diff --git a/src/Aspire.Cli/Projects/AppHostServerProject.cs b/src/Aspire.Cli/Projects/AppHostServerProject.cs index 7e4a40d38bd..a0aacb7bef7 100644 --- a/src/Aspire.Cli/Projects/AppHostServerProject.cs +++ b/src/Aspire.Cli/Projects/AppHostServerProject.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Security.Cryptography; -using System.Text; using Aspire.Cli.Bundles; using Aspire.Cli.Configuration; using Aspire.Cli.DotNet; @@ -36,28 +34,7 @@ internal sealed class AppHostServerProjectFactory( { public async Task CreateAsync(string appPath, CancellationToken cancellationToken = default) { - // Normalize the path - var normalizedPath = Path.GetFullPath(appPath); - normalizedPath = new Uri(normalizedPath).LocalPath; - normalizedPath = OperatingSystem.IsWindows() ? normalizedPath.ToLowerInvariant() : normalizedPath; - - // Generate socket path based on app path hash (deterministic for same project) - var pathHash = SHA256.HashData(Encoding.UTF8.GetBytes(normalizedPath)); - var socketName = Convert.ToHexString(pathHash)[..12].ToLowerInvariant() + ".sock"; - - string socketPath; - if (OperatingSystem.IsWindows()) - { - // Windows uses named pipes - socketPath = socketName; - } - else - { - // Unix uses domain sockets - var socketDir = Path.Combine(Path.GetTempPath(), ".aspire", "sockets"); - Directory.CreateDirectory(socketDir); - socketPath = Path.Combine(socketDir, socketName); - } + var socketPath = CliPathHelper.CreateGuestAppHostSocketPath("apphost.sock"); // Priority 1: Check for dev mode (ASPIRE_REPO_ROOT or running from Aspire source repo) var repoRoot = AspireRepositoryDetector.DetectRepositoryRoot(appPath); diff --git a/src/Aspire.Cli/Projects/AppHostServerSession.cs b/src/Aspire.Cli/Projects/AppHostServerSession.cs index e1376820cb0..b26e84e191b 100644 --- a/src/Aspire.Cli/Projects/AppHostServerSession.cs +++ b/src/Aspire.Cli/Projects/AppHostServerSession.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using Aspire.Cli.Configuration; using Aspire.Cli.Utils; +using Aspire.Hosting; using Microsoft.Extensions.Logging; namespace Aspire.Cli.Projects; @@ -13,6 +14,7 @@ namespace Aspire.Cli.Projects; /// internal sealed class AppHostServerSession : IAppHostServerSession { + private readonly string _authenticationToken; private readonly ILogger _logger; private readonly Process _serverProcess; private readonly OutputCollector _output; @@ -24,11 +26,13 @@ internal AppHostServerSession( Process serverProcess, OutputCollector output, string socketPath, + string authenticationToken, ILogger logger) { _serverProcess = serverProcess; _output = output; _socketPath = socketPath; + _authenticationToken = authenticationToken; _logger = logger; } @@ -41,12 +45,52 @@ internal AppHostServerSession( /// public OutputCollector Output => _output; + /// + /// Gets the authentication token for the server session. + /// + public string AuthenticationToken => _authenticationToken; + + /// + /// Starts an AppHost server process with an authentication token injected into the server environment. + /// + /// The server project to run. + /// The environment variables to pass to the server. + /// Whether to enable debug logging for the server. + /// The logger to use for lifecycle diagnostics. + /// The started AppHost server session. + internal static AppHostServerSession Start( + IAppHostServerProject appHostServerProject, + Dictionary? environmentVariables, + bool debug, + ILogger logger) + { + var currentPid = Environment.ProcessId; + var serverEnvironmentVariables = environmentVariables is null + ? new Dictionary() + : new Dictionary(environmentVariables); + + var authenticationToken = TokenGenerator.GenerateToken(); + serverEnvironmentVariables[KnownConfigNames.RemoteAppHostToken] = authenticationToken; + + var (socketPath, serverProcess, serverOutput) = appHostServerProject.Run( + currentPid, + serverEnvironmentVariables, + debug: debug); + + return new AppHostServerSession( + serverProcess, + serverOutput, + socketPath, + authenticationToken, + logger); + } + /// public async Task GetRpcClientAsync(CancellationToken cancellationToken) { ObjectDisposedException.ThrowIf(_disposed, nameof(AppHostServerSession)); - return _rpcClient ??= await AppHostRpcClient.ConnectAsync(_socketPath, cancellationToken); + return _rpcClient ??= await AppHostRpcClient.ConnectAsync(_socketPath, _authenticationToken, cancellationToken); } /// @@ -119,18 +163,10 @@ public async Task CreateAsync( ChannelName: prepareResult.ChannelName); } - // Start the server process - var currentPid = Environment.ProcessId; - var (socketPath, serverProcess, serverOutput) = appHostServerProject.Run( - currentPid, + var session = AppHostServerSession.Start( + appHostServerProject, launchSettingsEnvVars, - debug: debug); - - // Create the session - var session = new AppHostServerSession( - serverProcess, - serverOutput, - socketPath, + debug, _logger); return new AppHostServerSessionResult( diff --git a/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs b/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs index eb58875f4a7..9b9663ba0f4 100644 --- a/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs +++ b/src/Aspire.Cli/Projects/DotNetBasedAppHostServerProject.cs @@ -22,7 +22,6 @@ namespace Aspire.Cli.Projects; internal sealed class DotNetBasedAppHostServerProject : IAppHostServerProject { private const string ProjectHashFileName = ".projecthash"; - private const string FolderPrefix = ".aspire"; private const string AppsFolder = "hosts"; public const string ProjectFileName = "AppHostServer.csproj"; private const string ProjectDllName = "AppHostServer.dll"; @@ -74,20 +73,7 @@ public DotNetBasedAppHostServerProject( else { var pathDir = Convert.ToHexString(pathHash)[..12].ToLowerInvariant(); - var tempPath = Path.GetTempPath(); - // On macOS, /var is a symlink to /private/var. MSBuild resolves symlinks when - // computing relative paths between projects, but then tries to resolve those - // relative paths from the original (unresolved) directory. This mismatch causes - // transitive project references to fail. Using the canonical path ensures consistency. - if (OperatingSystem.IsMacOS() && tempPath.StartsWith("/var/", StringComparison.Ordinal)) - { - var canonical = "/private" + tempPath; - if (Directory.Exists(canonical)) - { - tempPath = canonical; - } - } - _projectModelPath = Path.Combine(tempPath, FolderPrefix, AppsFolder, pathDir); + _projectModelPath = Path.Combine(CliPathHelper.GetAspireHomeDirectory(), AppsFolder, pathDir); } // Create a stable UserSecretsId based on the app path hash @@ -97,7 +83,7 @@ public DotNetBasedAppHostServerProject( } /// - public string AppPath => _appPath; + public string AppDirectoryPath => _appPath; public string ProjectModelPath => _projectModelPath; public string UserSecretsId => _userSecretsId; diff --git a/src/Aspire.Cli/Projects/GuestAppHostProject.cs b/src/Aspire.Cli/Projects/GuestAppHostProject.cs index 2f3ba592b13..1024b27398c 100644 --- a/src/Aspire.Cli/Projects/GuestAppHostProject.cs +++ b/src/Aspire.Cli/Projects/GuestAppHostProject.cs @@ -37,7 +37,6 @@ internal sealed class GuestAppHostProject : IAppHostProject, IGuestAppHostSdkGen private readonly IDotNetCliRunner _runner; private readonly IPackagingService _packagingService; private readonly IConfiguration _configuration; - private readonly IConfigurationService _configurationService; private readonly IFeatures _features; private readonly ILanguageDiscovery _languageDiscovery; private readonly ILogger _logger; @@ -58,7 +57,6 @@ public GuestAppHostProject( IDotNetCliRunner runner, IPackagingService packagingService, IConfiguration configuration, - IConfigurationService configurationService, IFeatures features, ILanguageDiscovery languageDiscovery, ILogger logger, @@ -73,7 +71,6 @@ public GuestAppHostProject( _runner = runner; _packagingService = packagingService; _configuration = configuration; - _configurationService = configurationService; _features = features; _languageDiscovery = languageDiscovery; _logger = logger; @@ -167,27 +164,27 @@ private async Task> GetIntegrationReferencesAsync( /// /// Resolves the directory containing the nearest aspire.config.json (or legacy settings file) - /// by using the already-resolved path from . + /// by searching upward from . /// Falls back to when no config file is found. /// - private DirectoryInfo GetConfigDirectory(DirectoryInfo appHostDirectory) + private static DirectoryInfo GetConfigDirectory(DirectoryInfo appHostDirectory) { - var settingsFilePath = _configurationService.GetSettingsFilePath(isGlobal: false); - var settingsFile = new FileInfo(settingsFilePath); - - // If the settings file exists and has a parent directory, use that - if (settingsFile.Directory is { Exists: true }) + // Search from the apphost's directory upward to find the nearest config file. + var nearAppHost = ConfigurationHelper.FindNearestConfigFilePath(appHostDirectory); + if (nearAppHost is not null) { - // For legacy .aspire/settings.json, the config directory is the parent of .aspire/ - // TODO: Remove legacy .aspire/ check once confident most users have migrated. - // Tracked by https://github.com/microsoft/aspire/issues/15239 - if (string.Equals(settingsFile.Directory.Name, ".aspire", StringComparison.OrdinalIgnoreCase) - && settingsFile.Directory.Parent is not null) + var configFile = new FileInfo(nearAppHost); + if (configFile.Directory is { Exists: true }) { - return settingsFile.Directory.Parent; - } + // For legacy .aspire/settings.json, the config directory is the parent of .aspire/ + if (string.Equals(configFile.Directory.Name, ".aspire", StringComparison.OrdinalIgnoreCase) + && configFile.Directory.Parent is not null) + { + return configFile.Directory.Parent; + } - return settingsFile.Directory; + return configFile.Directory; + } } return appHostDirectory; @@ -209,7 +206,7 @@ private AspireConfigFile LoadConfiguration(DirectoryInfo directory) } } - private void SaveConfiguration(AspireConfigFile config, DirectoryInfo directory) + private static void SaveConfiguration(AspireConfigFile config, DirectoryInfo directory) { var configDir = GetConfigDirectory(directory); config.Save(configDir.FullName); @@ -258,41 +255,26 @@ internal async Task BuildAndGenerateSdkAsync(DirectoryInfo directory, Canc } // Step 2: Start the AppHost server temporarily for code generation - var currentPid = Environment.ProcessId; - var (socketPath, serverProcess, _) = appHostServerProject.Run(currentPid); - - try - { - // Step 3: Connect to server - await using var rpcClient = await AppHostRpcClient.ConnectAsync(socketPath, cancellationToken); - - // Step 4: Install dependencies using GuestRuntime (best effort - don't block code generation) - await InstallDependenciesAsync(directory, rpcClient, cancellationToken); - - // Step 5: Generate SDK code via RPC - await GenerateCodeViaRpcAsync( - directory.FullName, - rpcClient, - integrations, - cancellationToken); - - return true; - } - finally - { - // Step 6: Stop the server (we were just generating code) - if (!serverProcess.HasExited) - { - try - { - serverProcess.Kill(entireProcessTree: true); - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Error killing AppHost server process after code generation"); - } - } - } + await using var serverSession = AppHostServerSession.Start( + appHostServerProject, + environmentVariables: null, + debug: false, + _logger); + + // Step 3: Connect to server + var rpcClient = await serverSession.GetRpcClientAsync(cancellationToken); + + // Step 4: Install dependencies using GuestRuntime (best effort - don't block code generation) + await InstallDependenciesAsync(directory, rpcClient, cancellationToken); + + // Step 5: Generate SDK code via RPC + await GenerateCodeViaRpcAsync( + directory.FullName, + rpcClient, + integrations, + cancellationToken); + + return true; } Task IGuestAppHostSdkGenerator.BuildAndGenerateSdkAsync(DirectoryInfo directory, CancellationToken cancellationToken) @@ -423,8 +405,15 @@ public async Task RunAsync(AppHostProjectContext context, CancellationToken var enableHotReload = _features.IsFeatureEnabled(KnownFeatures.DefaultWatchEnabled, defaultValue: false); // Start the AppHost server process - var currentPid = Environment.ProcessId; - var (socketPath, appHostServerProcess, appHostServerOutputCollector) = appHostServerProject.Run(currentPid, launchSettingsEnvVars, debug: context.Debug); + await using var serverSession = AppHostServerSession.Start( + appHostServerProject, + launchSettingsEnvVars, + context.Debug, + _logger); + var socketPath = serverSession.SocketPath; + var appHostServerProcess = serverSession.ServerProcess; + var appHostServerOutputCollector = serverSession.Output; + var authenticationToken = serverSession.AuthenticationToken; // The backchannel completion source is the contract with RunCommand // We signal this when the backchannel is ready, RunCommand uses it for UX @@ -444,7 +433,7 @@ public async Task RunAsync(AppHostProjectContext context, CancellationToken } // Step 5: Connect to server for RPC calls - await using var rpcClient = await AppHostRpcClient.ConnectAsync(socketPath, cancellationToken); + var rpcClient = await serverSession.GetRpcClientAsync(cancellationToken); // Step 6: Install dependencies (using GuestRuntime) // The GuestRuntime will skip if the RuntimeSpec doesn't have InstallDependencies configured @@ -486,7 +475,8 @@ await GenerateCodeViaRpcAsync( { ["REMOTE_APP_HOST_SOCKET_PATH"] = socketPath, ["ASPIRE_PROJECT_DIRECTORY"] = directory.FullName, - ["ASPIRE_APPHOST_FILEPATH"] = appHostFile.FullName + ["ASPIRE_APPHOST_FILEPATH"] = appHostFile.FullName, + [KnownConfigNames.RemoteAppHostToken] = authenticationToken }; // Check if the extension should launch the guest app host (for VS Code debugging). @@ -788,8 +778,15 @@ public async Task PublishAsync(PublishContext context, CancellationToken ca launchSettingsEnvVars[KnownConfigNames.AspireUserSecretsId] = UserSecretsPathHelper.ComputeSyntheticUserSecretsId(appHostFile.FullName); // Step 2: Start the AppHost server process(it opens the backchannel for progress reporting) - var currentPid = Environment.ProcessId; - var (jsonRpcSocketPath, appHostServerProcess, appHostServerOutputCollector) = appHostServerProject.Run(currentPid, launchSettingsEnvVars, debug: context.Debug); + await using var serverSession = AppHostServerSession.Start( + appHostServerProject, + launchSettingsEnvVars, + context.Debug, + _logger); + var jsonRpcSocketPath = serverSession.SocketPath; + var appHostServerProcess = serverSession.ServerProcess; + var appHostServerOutputCollector = serverSession.Output; + var authenticationToken = serverSession.AuthenticationToken; // Start connecting to the backchannel if (context.BackchannelCompletionSource is not null) @@ -808,7 +805,7 @@ public async Task PublishAsync(PublishContext context, CancellationToken ca } // Step 3: Connect to server for RPC calls - await using var rpcClient = await AppHostRpcClient.ConnectAsync(jsonRpcSocketPath, cancellationToken); + var rpcClient = await serverSession.GetRpcClientAsync(cancellationToken); // Step 4: Install dependencies if needed (using GuestRuntime) // The GuestRuntime will skip if the RuntimeSpec doesn't have InstallDependencies configured @@ -848,7 +845,8 @@ await GenerateCodeViaRpcAsync( { ["REMOTE_APP_HOST_SOCKET_PATH"] = jsonRpcSocketPath, ["ASPIRE_PROJECT_DIRECTORY"] = directory.FullName, - ["ASPIRE_APPHOST_FILEPATH"] = appHostFile.FullName + ["ASPIRE_APPHOST_FILEPATH"] = appHostFile.FullName, + [KnownConfigNames.RemoteAppHostToken] = authenticationToken }; // Step 6: Execute the guest apphost for publishing @@ -924,11 +922,7 @@ await GenerateCodeViaRpcAsync( /// private static string GetBackchannelSocketPath() { - var homeDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - var aspireCliPath = Path.Combine(homeDirectory, ".aspire", "cli", "backchannels"); - Directory.CreateDirectory(aspireCliPath); - var socketName = $"{Guid.NewGuid():N}.sock"; - return Path.Combine(aspireCliPath, socketName); + return CliPathHelper.CreateUnixDomainSocketPath("cli.sock"); } /// diff --git a/src/Aspire.Cli/Projects/IAppHostRpcClient.cs b/src/Aspire.Cli/Projects/IAppHostRpcClient.cs index 371cb734413..445faaa879d 100644 --- a/src/Aspire.Cli/Projects/IAppHostRpcClient.cs +++ b/src/Aspire.Cli/Projects/IAppHostRpcClient.cs @@ -69,5 +69,5 @@ internal interface IAppHostRpcClientFactory /// Creates and connects an RPC client to the specified socket path. /// Handles platform-specific connection (Unix sockets vs named pipes). /// - Task ConnectAsync(string socketPath, CancellationToken cancellationToken); + Task ConnectAsync(string socketPath, string authenticationToken, CancellationToken cancellationToken); } diff --git a/src/Aspire.Cli/Projects/IAppHostServerProject.cs b/src/Aspire.Cli/Projects/IAppHostServerProject.cs index 8c19179b308..a5fc0730826 100644 --- a/src/Aspire.Cli/Projects/IAppHostServerProject.cs +++ b/src/Aspire.Cli/Projects/IAppHostServerProject.cs @@ -31,7 +31,7 @@ internal interface IAppHostServerProject /// /// Gets the path to the user's app (the polyglot apphost directory). /// - string AppPath { get; } + string AppDirectoryPath { get; } /// /// Prepares the AppHost server for running. diff --git a/src/Aspire.Cli/Projects/IAppHostServerSession.cs b/src/Aspire.Cli/Projects/IAppHostServerSession.cs index 8668a92796c..8fce43a2ec0 100644 --- a/src/Aspire.Cli/Projects/IAppHostServerSession.cs +++ b/src/Aspire.Cli/Projects/IAppHostServerSession.cs @@ -29,6 +29,11 @@ internal interface IAppHostServerSession : IAsyncDisposable /// OutputCollector Output { get; } + /// + /// Gets the authentication token for the server session. + /// + string AuthenticationToken { get; } + /// /// Gets an RPC client connected to this session. /// diff --git a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs index 7bfdb9e3100..5c05e2a66d7 100644 --- a/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs +++ b/src/Aspire.Cli/Projects/PrebuiltAppHostServer.cs @@ -24,7 +24,7 @@ namespace Aspire.Cli.Projects; /// internal sealed class PrebuiltAppHostServer : IAppHostServerProject { - private readonly string _appPath; + private readonly string _appDirectoryPath; private readonly string _socketPath; private readonly LayoutConfiguration _layout; private readonly BundleNuGetService _nugetService; @@ -41,7 +41,7 @@ internal sealed class PrebuiltAppHostServer : IAppHostServerProject /// /// Initializes a new instance of the PrebuiltAppHostServer class. /// - /// The path to the user's polyglot app host directory. + /// The path to the user's polyglot app host directory (must be a directory path). /// The socket path for JSON-RPC communication. /// The bundle layout configuration. /// The NuGet service for restoring integration packages (NuGet-only path). @@ -61,7 +61,7 @@ public PrebuiltAppHostServer( IConfigurationService configurationService, ILogger logger) { - _appPath = Path.GetFullPath(appPath); + _appDirectoryPath = Path.GetFullPath(appPath); _socketPath = socketPath; _layout = layout; _nugetService = nugetService; @@ -72,14 +72,14 @@ public PrebuiltAppHostServer( _logger = logger; // Create a working directory for this app host session - var pathHash = SHA256.HashData(Encoding.UTF8.GetBytes(_appPath)); + var pathHash = SHA256.HashData(Encoding.UTF8.GetBytes(_appDirectoryPath)); var pathDir = Convert.ToHexString(pathHash)[..12].ToLowerInvariant(); - _workingDirectory = Path.Combine(Path.GetTempPath(), ".aspire", "bundle-hosts", pathDir); + _workingDirectory = Path.Combine(CliPathHelper.GetAspireHomeDirectory(), "bundle-hosts", pathDir); Directory.CreateDirectory(_workingDirectory); } /// - public string AppPath => _appPath; + public string AppDirectoryPath => _appDirectoryPath; /// /// Gets the path to the aspire-managed executable (used as the server). @@ -171,13 +171,11 @@ private async Task RestoreNuGetPackagesAsync( var packages = packageRefs.Select(r => (r.Name, r.Version!)).ToList(); var sources = await GetNuGetSourcesAsync(channelName, cancellationToken); - var appHostDirectory = Path.GetDirectoryName(_appPath); - return await _nugetService.RestorePackagesAsync( packages, DotNetBasedAppHostServerProject.TargetFramework, sources: sources, - workingDirectory: appHostDirectory, + workingDirectory: _appDirectoryPath, ct: cancellationToken); } @@ -337,7 +335,7 @@ internal static string GenerateIntegrationProjectFile( private async Task ResolveChannelNameAsync(CancellationToken cancellationToken) { // Check local settings.json first - var localConfig = AspireJsonConfiguration.Load(Path.GetDirectoryName(_appPath)!); + var localConfig = AspireJsonConfiguration.Load(_appDirectoryPath); var channelName = localConfig?.Channel; // Fall back to global config @@ -506,7 +504,7 @@ internal static string GenerateIntegrationProjectFile( } /// - public string GetInstanceIdentifier() => _appPath; + public string GetInstanceIdentifier() => _appDirectoryPath; /// /// Reads the project reference assembly names written by the MSBuild target during build. diff --git a/src/Aspire.Cli/Projects/ProjectLocator.cs b/src/Aspire.Cli/Projects/ProjectLocator.cs index 498befe22a1..aa1f6dc5ee6 100644 --- a/src/Aspire.Cli/Projects/ProjectLocator.cs +++ b/src/Aspire.Cli/Projects/ProjectLocator.cs @@ -406,6 +406,47 @@ public async Task UseOrFindAppHostProjectFileAsync(F private async Task CreateSettingsFileAsync(FileInfo projectFile, CancellationToken cancellationToken) { + // Search from the apphost's directory upward for an existing config file. + // This handles the case where "aspire new" created a project in a subdirectory + // and the user runs "aspire run" from the parent without cd-ing first. + if (projectFile.Directory is { } appHostDir) + { + var nearAppHost = ConfigurationHelper.FindNearestConfigFilePath(appHostDir); + if (nearAppHost is not null) + { + var configDir = Path.GetDirectoryName(nearAppHost)!; + + // For legacy .aspire/settings.json, the config root is the parent of .aspire/ + var trimmedConfigDir = configDir.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); + if (string.Equals(Path.GetFileName(trimmedConfigDir), ".aspire", StringComparison.OrdinalIgnoreCase)) + { + var parentDir = Directory.GetParent(trimmedConfigDir); + if (parentDir is not null) + { + configDir = parentDir.FullName; + } + } + + var existingConfig = AspireConfigFile.Load(configDir); + if (existingConfig?.AppHost?.Path is { } existingPath) + { + // Resolve the stored path relative to the config file's directory. + var resolvedPath = Path.GetFullPath( + Path.IsPathRooted(existingPath) ? existingPath : Path.Combine(configDir, existingPath)); + + // Only skip creation if the config already points to the discovered apphost. + // If the path is stale/invalid, fall through so the config gets healed. + if (string.Equals(resolvedPath, projectFile.FullName, StringComparison.OrdinalIgnoreCase)) + { + logger.LogDebug( + "Config at {Path} already references apphost {AppHost}, skipping creation", + nearAppHost, projectFile.FullName); + return; + } + } + } + } + var settingsFile = GetOrCreateLocalAspireConfigFile(); var fileExisted = settingsFile.Exists; diff --git a/src/Aspire.Cli/Projects/ProjectUpdater.cs b/src/Aspire.Cli/Projects/ProjectUpdater.cs index 735d3ddbaff..647011463bd 100644 --- a/src/Aspire.Cli/Projects/ProjectUpdater.cs +++ b/src/Aspire.Cli/Projects/ProjectUpdater.cs @@ -115,7 +115,7 @@ public async Task UpdateProjectAsync(FileInfo projectFile, cancellationToken: cancellationToken); var nugetConfigDirectory = new DirectoryInfo(selectedPathForNewNuGetConfigFile); - await NuGetConfigMerger.CreateOrUpdateAsync(nugetConfigDirectory, channel, AnalyzeAndConfirmNuGetConfigChanges, cancellationToken); + await NuGetConfigMerger.CreateOrUpdateAsync(nugetConfigDirectory, channel, AnalyzeAndConfirmNuGetConfigChanges, cancellationToken: cancellationToken); } interactionService.DisplayEmptyLine(); diff --git a/src/Aspire.Cli/Resources/ErrorStrings.resx b/src/Aspire.Cli/Resources/ErrorStrings.resx index 5ae0e332b1c..89b3cb333ad 100644 --- a/src/Aspire.Cli/Resources/ErrorStrings.resx +++ b/src/Aspire.Cli/Resources/ErrorStrings.resx @@ -118,7 +118,7 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. + C# apphost requires .NET SDK version {0} or later. Detected: {1}. Invalid locale {0} provided will not be used. diff --git a/src/Aspire.Cli/Resources/InteractionServiceStrings.Designer.cs b/src/Aspire.Cli/Resources/InteractionServiceStrings.Designer.cs index eae0d74ab33..ce5559366aa 100644 --- a/src/Aspire.Cli/Resources/InteractionServiceStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/InteractionServiceStrings.Designer.cs @@ -249,6 +249,15 @@ public static string ProjectCouldNotBeBuilt { } } + /// + /// Looks up a localized string similar to The app could not be created. See logs at {0}. + /// + public static string ProjectCouldNotBeCreated { + get { + return ResourceManager.GetString("ProjectCouldNotBeCreated", resourceCulture); + } + } + /// /// Looks up a localized string similar to The --apphost option specified a project that does not exist.. /// diff --git a/src/Aspire.Cli/Resources/InteractionServiceStrings.resx b/src/Aspire.Cli/Resources/InteractionServiceStrings.resx index d3e5a106d08..37147bc2a6a 100644 --- a/src/Aspire.Cli/Resources/InteractionServiceStrings.resx +++ b/src/Aspire.Cli/Resources/InteractionServiceStrings.resx @@ -188,6 +188,9 @@ The project could not be built. See logs at {0} + + The app could not be created. See logs at {0} + The operation was canceled. diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.cs.xlf index 526bbb28fad..5ab35771074 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.cs.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - Rozhraní příkazového řádku Aspire vyžaduje sadu .NET SDK verze {0} nebo novější. Zjištěno: {1}. + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + Rozhraní příkazového řádku Aspire vyžaduje sadu .NET SDK verze {0} nebo novější. Zjištěno: {1}. diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.de.xlf index 948f7645b94..1f5ce79cce2 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.de.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - Die Aspire-CLI erfordert .NET SDK-Version {0} oder höher. Erkannt: {1}. + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + Die Aspire-CLI erfordert .NET SDK-Version {0} oder höher. Erkannt: {1}. diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.es.xlf index 36facbd189f..6c59080d8bb 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.es.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - La CLI de Aspire requiere la versión {0} del SDK de .NET o posterior. Detectado: {1}. + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + La CLI de Aspire requiere la versión {0} del SDK de .NET o posterior. Detectado: {1}. diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.fr.xlf index 993522cbd44..19a1a4584b7 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.fr.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - L’interface CLI Aspire nécessite le kit SDK .NET version {0} ou ultérieure. A détecté : {1}. + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + L’interface CLI Aspire nécessite le kit SDK .NET version {0} ou ultérieure. A détecté : {1}. diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.it.xlf index 827c1d97eec..e13d64b5401 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.it.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - L'interfaccia della riga di comando di Aspire richiede .NET SDK versione {0} o successiva. Rilevata: {1}. + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + L'interfaccia della riga di comando di Aspire richiede .NET SDK versione {0} o successiva. Rilevata: {1}. diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.ja.xlf index 5c485b91f98..9fb0c163dab 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.ja.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - Aspire CLI には .NET SDK バージョン {0} 以降が必要です。以下を検出しました: {1}。 + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + Aspire CLI には .NET SDK バージョン {0} 以降が必要です。以下を検出しました: {1}。 diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.ko.xlf index 94e6c00b19d..b73c153b7e0 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.ko.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - Aspire CLI를 사용하려면 .NET SDK 버전 {0} 이상이 필요합니다. 감지됨: {1}. + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + Aspire CLI를 사용하려면 .NET SDK 버전 {0} 이상이 필요합니다. 감지됨: {1}. diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.pl.xlf index 01eb2784c00..9649facbc0e 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.pl.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - Interfejs wiersza polecenia platformy Aspire wymaga zestawu .NET SDK w wersji {0} lub nowszej. Wykryto {1}. + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + Interfejs wiersza polecenia platformy Aspire wymaga zestawu .NET SDK w wersji {0} lub nowszej. Wykryto {1}. diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.pt-BR.xlf index b4a150afcff..b8e77aa5239 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.pt-BR.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - A CLI do Aspire requer a versão do SDK do .NET {0} ou posterior. Detectado: {1}. + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + A CLI do Aspire requer a versão do SDK do .NET {0} ou posterior. Detectado: {1}. diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.ru.xlf index 27d4bdab841..130a9ef411a 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.ru.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - Для Aspire CLI требуется версия .NET SDK {0} или более поздняя. Обнаружено: {1}. + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + Для Aspire CLI требуется версия .NET SDK {0} или более поздняя. Обнаружено: {1}. diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.tr.xlf index 966a8b05c49..aa280440399 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.tr.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - Aspire CLI için .NET SDK sürümünün {0} veya daha yeni olması gerekiyor. Algılanan: {1}. + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + Aspire CLI için .NET SDK sürümünün {0} veya daha yeni olması gerekiyor. Algılanan: {1}. diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.zh-Hans.xlf index bea123b9ec8..ef5272c32cb 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.zh-Hans.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - Aspire CLI 需要 .NET SDK {0} 或更高版本。检测到: {1}。 + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + Aspire CLI 需要 .NET SDK {0} 或更高版本。检测到: {1}。 diff --git a/src/Aspire.Cli/Resources/xlf/ErrorStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/ErrorStrings.zh-Hant.xlf index ab2a1a024f5..7a8b9fda6d4 100644 --- a/src/Aspire.Cli/Resources/xlf/ErrorStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/ErrorStrings.zh-Hant.xlf @@ -123,8 +123,8 @@ - The Aspire CLI requires .NET SDK version {0} or later. Detected: {1}. - Aspire CLI 需要 .NET SDK 版本 {0} 或更新版本。已偵測到: {1}。 + C# apphost requires .NET SDK version {0} or later. Detected: {1}. + Aspire CLI 需要 .NET SDK 版本 {0} 或更新版本。已偵測到: {1}。 diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.cs.xlf index ec054fb2ede..08cb6eacad6 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.cs.xlf @@ -102,6 +102,11 @@ Projekt se nepovedlo sestavit. Protokoly najdete na {0} + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. Parametr --apphost určil projekt, který neexistuje. diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.de.xlf index bc91e9c2eac..987dd948575 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.de.xlf @@ -102,6 +102,11 @@ Das Projekt konnte nicht erstellt werden. Protokolle unter {0} anzeigen. + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. Die Option „--apphost“ hat ein Projekt angegeben, das nicht vorhanden ist. diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.es.xlf index defa5bbd067..879e97d0ba2 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.es.xlf @@ -102,6 +102,11 @@ No se pudo compilar el proyecto. Consulte los registros en {0} + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. La opción --apphost especificó un proyecto que no existe. diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.fr.xlf index be651d0f3e0..e68798c8aeb 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.fr.xlf @@ -102,6 +102,11 @@ Désolé, le projet n’a pas pu être généré. Consultez les journaux à l’emplacement {0} + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. L’option --apphost spécifie un projet qui n’existe pas. diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.it.xlf index 0c652ffb1d5..a0af6a125a1 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.it.xlf @@ -102,6 +102,11 @@ Non è possibile compilare il progetto. Consultare i log in {0} + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. L'opzione --apphost ha specificato un progetto che non esiste. diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ja.xlf index 8c0287b1255..db3559245dd 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ja.xlf @@ -102,6 +102,11 @@ プロジェクトを構築できませんでした。{0} でログを表示する + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. --apphost オプションで、存在しないプロジェクトが指定されています。 diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ko.xlf index 81977320c23..3916fd3fac6 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ko.xlf @@ -102,6 +102,11 @@ 프로젝트를 빌드할 수 없습니다. {0}에서 로그 보기 + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. --apphost 옵션에서 존재하지 않는 프로젝트를 지정했습니다. diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.pl.xlf index faaf85acdc8..bdaa859a122 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.pl.xlf @@ -102,6 +102,11 @@ Nie można skompilować projektu. Zobacz dzienniki pod adresem {0} + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. Opcja --apphost określiła projekt, który nie istnieje. diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.pt-BR.xlf index f22dab41151..13931fab900 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.pt-BR.xlf @@ -102,6 +102,11 @@ O projeto não pôde ser compilado. Ver logs em {0} + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. A opção --apphost especificou um projeto que não existe. diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ru.xlf index adf5cdd3c4a..cfe2e10dde4 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.ru.xlf @@ -102,6 +102,11 @@ Не удалось выполнить сборку проекта. См. журналы по адресу {0} + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. Параметр --apphost указывал на проект, которого не существует. diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.tr.xlf index c7ca1c54c35..cace162ace7 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.tr.xlf @@ -102,6 +102,11 @@ Proje oluşturulamadı. {0} adresinde günlüklere bakın + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. --apphost seçeneği var olmayan bir projeyi belirtti. diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.zh-Hans.xlf index 4a9e570ca66..96eca108389 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.zh-Hans.xlf @@ -102,6 +102,11 @@ 无法生成项目。请参阅 {0} 处的日志 + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. --apphost 选项指定了不存在的项目。 diff --git a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.zh-Hant.xlf index 5a26b14f5a4..0ce1651a412 100644 --- a/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/InteractionServiceStrings.zh-Hant.xlf @@ -102,6 +102,11 @@ 無法建置專案。請參閱 {0} 中的記錄 + + The app could not be created. See logs at {0} + The app could not be created. See logs at {0} + + The --apphost option specified a project that does not exist. --apphost 選項指定的專案不存在。 diff --git a/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs b/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs index 566fec78312..9b8dffb1fd1 100644 --- a/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs +++ b/src/Aspire.Cli/Scaffolding/ScaffoldingService.cs @@ -79,95 +79,80 @@ private async Task ScaffoldGuestLanguageAsync(ScaffoldContext context, Can } // Step 2: Start the server temporarily for scaffolding and code generation - var currentPid = Environment.ProcessId; - var (socketPath, serverProcess, _) = appHostServerProject.Run(currentPid, new Dictionary()); - - try + await using var serverSession = AppHostServerSession.Start( + appHostServerProject, + environmentVariables: null, + debug: false, + _logger); + + // Step 3: Connect to server and get scaffold templates via RPC + var rpcClient = await serverSession.GetRpcClientAsync(cancellationToken); + + var scaffoldFiles = await rpcClient.ScaffoldAppHostAsync( + language.LanguageId, + directory.FullName, + context.ProjectName, + cancellationToken); + + // Step 4: Write scaffold files to disk + foreach (var (fileName, content) in scaffoldFiles) { - // Step 3: Connect to server and get scaffold templates via RPC - await using var rpcClient = await AppHostRpcClient.ConnectAsync(socketPath, cancellationToken); - - var scaffoldFiles = await rpcClient.ScaffoldAppHostAsync( - language.LanguageId, - directory.FullName, - context.ProjectName, - cancellationToken); - - // Step 4: Write scaffold files to disk - foreach (var (fileName, content) in scaffoldFiles) + var filePath = Path.Combine(directory.FullName, fileName); + var fileDirectory = Path.GetDirectoryName(filePath); + if (!string.IsNullOrEmpty(fileDirectory)) { - var filePath = Path.Combine(directory.FullName, fileName); - var fileDirectory = Path.GetDirectoryName(filePath); - if (!string.IsNullOrEmpty(fileDirectory)) - { - Directory.CreateDirectory(fileDirectory); - } - await File.WriteAllTextAsync(filePath, content, cancellationToken); + Directory.CreateDirectory(fileDirectory); } + await File.WriteAllTextAsync(filePath, content, cancellationToken); + } - _logger.LogDebug("Wrote {Count} scaffold files", scaffoldFiles.Count); + _logger.LogDebug("Wrote {Count} scaffold files", scaffoldFiles.Count); - // Step 5: Install dependencies using GuestRuntime - var installResult = await _interactionService.ShowStatusAsync( - $"Installing {language.DisplayName} dependencies...", - () => InstallDependenciesAsync(directory, language, rpcClient, cancellationToken), - emoji: KnownEmojis.Package); - if (installResult != 0) - { - return false; - } + // Step 5: Install dependencies using GuestRuntime + var installResult = await _interactionService.ShowStatusAsync( + $"Installing {language.DisplayName} dependencies...", + () => InstallDependenciesAsync(directory, language, rpcClient, cancellationToken), + emoji: KnownEmojis.Package); + if (installResult != 0) + { + return false; + } - // Step 6: Generate SDK code via RPC - await GenerateCodeViaRpcAsync( - directory.FullName, - rpcClient, - language, - cancellationToken); + // Step 6: Generate SDK code via RPC + await GenerateCodeViaRpcAsync( + directory.FullName, + rpcClient, + language, + cancellationToken); - // Save channel and language to aspire.config.json (new format) - // Read profiles from apphost.run.json (created by codegen) and merge into aspire.config.json - var appHostRunPath = Path.Combine(directory.FullName, "apphost.run.json"); - var profiles = AspireConfigFile.ReadApphostRunProfiles(appHostRunPath, _logger); + // Save channel and language to aspire.config.json (new format) + // Read profiles from apphost.run.json (created by codegen) and merge into aspire.config.json + var appHostRunPath = Path.Combine(directory.FullName, "apphost.run.json"); + var profiles = AspireConfigFile.ReadApphostRunProfiles(appHostRunPath, _logger); - if (profiles is not null && File.Exists(appHostRunPath)) + if (profiles is not null && File.Exists(appHostRunPath)) + { + try { - try - { - // Delete apphost.run.json since profiles are now in aspire.config.json - File.Delete(appHostRunPath); - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Failed to delete apphost.run.json after reading profiles"); - } + // Delete apphost.run.json since profiles are now in aspire.config.json + File.Delete(appHostRunPath); } - - config.Profiles = profiles; - if (prepareResult.ChannelName is not null) + catch (Exception ex) { - config.Channel = prepareResult.ChannelName; + _logger.LogDebug(ex, "Failed to delete apphost.run.json after reading profiles"); } - config.AppHost ??= new AspireConfigAppHost(); - config.AppHost.Path ??= language.AppHostFileName; - config.AppHost.Language = language.LanguageId; - config.Save(directory.FullName); - return true; } - finally + + config.Profiles = profiles; + if (prepareResult.ChannelName is not null) { - // Step 7: Stop the server - if (!serverProcess.HasExited) - { - try - { - serverProcess.Kill(entireProcessTree: true); - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Error killing AppHost server process after scaffolding"); - } - } + config.Channel = prepareResult.ChannelName; } + config.AppHost ??= new AspireConfigAppHost(); + config.AppHost.Path ??= language.AppHostFileName; + config.AppHost.Language = language.LanguageId; + config.Save(directory.FullName); + return true; } private async Task InstallDependenciesAsync( diff --git a/src/Aspire.Cli/Templating/Templates/ts-starter/frontend/src/App.tsx b/src/Aspire.Cli/Templating/Templates/ts-starter/frontend/src/App.tsx index b9b5a1b10ae..869f34c5cac 100644 --- a/src/Aspire.Cli/Templating/Templates/ts-starter/frontend/src/App.tsx +++ b/src/Aspire.Cli/Templating/Templates/ts-starter/frontend/src/App.tsx @@ -166,7 +166,7 @@ function App() { Learn more about Aspire (opens in new tab) BuildAppHostAsync(IDotNetCliRunner runner, IInte /// Computes the auxiliary backchannel socket path prefix for a given AppHost project file. /// /// - /// Since socket names now include the AppHost's PID (e.g., auxi.sock.{hash}.{pid}), + /// Since socket names now include a randomized instance hash and the AppHost's PID + /// (e.g., auxi.sock.{hash}.{instanceHash}.{pid}), /// the CLI cannot compute the exact socket path. Use this prefix with a glob pattern /// to find matching sockets, or use instead. /// @@ -106,15 +107,16 @@ internal static string[] FindMatchingSockets(string appHostPath, string homeDire /// Extracts the hash portion from an auxiliary socket path. /// /// - /// Works with both old format (auxi.sock.{hash}) and new format (auxi.sock.{hash}.{pid}). + /// Works with old format (auxi.sock.{hash}), previous format (auxi.sock.{hash}.{pid}), + /// and current format (auxi.sock.{hash}.{instanceHash}.{pid}). /// - /// The full socket path (e.g., "/path/to/auxi.sock.b67075ff12d56865.12345"). + /// The full socket path (e.g., "/path/to/auxi.sock.b67075ff12d56865.a1b2c3d4e5f6.12345"). /// The hash portion (e.g., "b67075ff12d56865"), or null if the format is unrecognized. internal static string? ExtractHashFromSocketPath(string socketPath) => BackchannelConstants.ExtractHash(socketPath); /// - /// Extracts the PID from an auxiliary socket path (new format only). + /// Extracts the PID from an auxiliary socket path when one is present. /// /// The full socket path. /// The PID if present and valid, or null for old format sockets. diff --git a/src/Aspire.Cli/Utils/CliPathHelper.cs b/src/Aspire.Cli/Utils/CliPathHelper.cs new file mode 100644 index 00000000000..48aedcb8e52 --- /dev/null +++ b/src/Aspire.Cli/Utils/CliPathHelper.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.Backchannel; + +namespace Aspire.Cli.Utils; + +internal static class CliPathHelper +{ + internal static string GetAspireHomeDirectory() + => Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".aspire"); + + /// + /// Creates a randomized CLI-managed socket path. + /// + /// The socket file prefix. + internal static string CreateUnixDomainSocketPath(string socketPrefix) + => CreateSocketPath(socketPrefix, isGuestAppHost: false); + + internal static string CreateGuestAppHostSocketPath(string socketPrefix) + => CreateSocketPath(socketPrefix, isGuestAppHost: true); + + private static string CreateSocketPath(string socketPrefix, bool isGuestAppHost) + { + var socketName = $"{socketPrefix}.{BackchannelConstants.CreateRandomIdentifier()}"; + + if (isGuestAppHost && OperatingSystem.IsWindows()) + { + return socketName; + } + + var socketDirectory = GetCliSocketDirectory(); + Directory.CreateDirectory(socketDirectory); + return Path.Combine(socketDirectory, socketName); + } + + private static string GetCliHomeDirectory() + => Path.Combine(GetAspireHomeDirectory(), "cli"); + + private static string GetCliRuntimeDirectory() + => Path.Combine(GetCliHomeDirectory(), "runtime"); + + private static string GetCliSocketDirectory() + => Path.Combine(GetCliRuntimeDirectory(), "sockets"); +} diff --git a/src/Aspire.Cli/Utils/ConfigurationHelper.cs b/src/Aspire.Cli/Utils/ConfigurationHelper.cs index 74e97bb8c5b..0875557b5c0 100644 --- a/src/Aspire.Cli/Utils/ConfigurationHelper.cs +++ b/src/Aspire.Cli/Utils/ConfigurationHelper.cs @@ -69,6 +69,34 @@ internal static string BuildPathToSettingsJsonFile(string workingDirectory) return Path.Combine(workingDirectory, ".aspire", "settings.json"); } + /// + /// Searches upward from for the nearest + /// aspire.config.json or legacy .aspire/settings.json. + /// + /// The full path to the config file, or null if none is found. + internal static string? FindNearestConfigFilePath(DirectoryInfo startDirectory) + { + var searchDir = startDirectory; + while (searchDir is not null) + { + var configPath = Path.Combine(searchDir.FullName, AspireConfigFile.FileName); + if (File.Exists(configPath)) + { + return configPath; + } + + var legacyPath = BuildPathToSettingsJsonFile(searchDir.FullName); + if (File.Exists(legacyPath)) + { + return legacyPath; + } + + searchDir = searchDir.Parent; + } + + return null; + } + /// /// Serializes a JsonObject and writes it to a settings file, creating the directory if needed. /// @@ -154,13 +182,15 @@ internal static bool TryNormalizeSettingsFile(string filePath) } // Find all colon-separated keys at root level - var colonKeys = new List<(string key, string? value)>(); + var colonKeys = new List<(string key, JsonNode? value)>(); foreach (var kvp in settings) { if (kvp.Key.Contains(':')) { - colonKeys.Add((kvp.Key, kvp.Value?.ToString())); + // DeepClone preserves the original JSON type (boolean, number, etc.) + // instead of converting to string via ToString(). + colonKeys.Add((kvp.Key, kvp.Value?.DeepClone())); } } diff --git a/src/Aspire.Dashboard/Model/Assistant/AssistantChatDataContext.cs b/src/Aspire.Dashboard/Model/Assistant/AssistantChatDataContext.cs index 8cebb29ba0f..1437a6d202a 100644 --- a/src/Aspire.Dashboard/Model/Assistant/AssistantChatDataContext.cs +++ b/src/Aspire.Dashboard/Model/Assistant/AssistantChatDataContext.cs @@ -102,10 +102,11 @@ public async Task GetTraceAsync( return SharedAIHelpers.GetTraceJson(spans, r => OtlpHelpers.GetResourceName(r, resources), AIHelpers.GetDashboardUrl(_dashboardOptions.CurrentValue)); } + // resourceName is provided as a non-nullable string to prevent the JSON schema from including an array. An array breaks VS integration. [Description("Get structured logs for resources.")] public async Task GetStructuredLogsAsync( [Description("The resource name. This limits logs returned to the specified resource. If no resource name is specified then structured logs for all resources are returned.")] - string? resourceName = null, + string resourceName, CancellationToken cancellationToken = default) { // TODO: The resourceName might be a name that resolves to multiple replicas, e.g. catalogservice has two replicas. @@ -147,10 +148,11 @@ public async Task GetStructuredLogsAsync( return response; } + // resourceName is provided as a non-nullable string to prevent the JSON schema from including an array. An array breaks VS integration. [Description("Get distributed traces for resources. A distributed trace is used to track operations. A distributed trace can span multiple resources across a distributed system. Includes a list of distributed traces with their IDs, resources in the trace, duration and whether an error occurred in the trace.")] public async Task GetTracesAsync( [Description("The resource name. This limits traces returned to the specified resource. If no resource name is specified then distributed traces for all resources are returned.")] - string? resourceName = null, + string resourceName, CancellationToken cancellationToken = default) { // TODO: The resourceName might be a name that resolves to multiple replicas, e.g. catalogservice has two replicas. diff --git a/src/Aspire.Dashboard/Model/Assistant/AssistantChatViewModel.cs b/src/Aspire.Dashboard/Model/Assistant/AssistantChatViewModel.cs index 21549f075b2..546ed86c59c 100644 --- a/src/Aspire.Dashboard/Model/Assistant/AssistantChatViewModel.cs +++ b/src/Aspire.Dashboard/Model/Assistant/AssistantChatViewModel.cs @@ -95,7 +95,7 @@ public sealed class AssistantChatViewModel : IDisposable private const int MaximumResponseLength = 1024 * 1024 * 10; // Small, cheap, fast model for follow up questions. private const string FollowUpQuestionsModel = "gpt-4o-mini"; - private static readonly string[] s_defaultModels = ["gpt-4.1", "gpt-4o"]; + private static readonly string[] s_defaultModels = ["gpt-5.4", "gpt-4.1", "gpt-4o"]; // Older models that VS Code returns as available models. Don't show them in the model selector. // There are better, cheaper alternatives that should always be used. private static readonly string[] s_oldModels = ["gpt-4", "gpt-4-turbo", "gpt-3.5-turbo"]; diff --git a/src/Aspire.Dashboard/wwwroot/css/app.css b/src/Aspire.Dashboard/wwwroot/css/app.css index 12d57be6191..64787b13cca 100644 --- a/src/Aspire.Dashboard/wwwroot/css/app.css +++ b/src/Aspire.Dashboard/wwwroot/css/app.css @@ -881,7 +881,7 @@ fluent-switch.table-switch::part(label) { /* Korean text can have bad wrapping behavior. See https://github.com/w3c/csswg-drafts/issues/4285 - This rule prevents Korean "Close" text in tooltip from wrapping. See https://github.com/dotnet/aspire/issues/8170 + This rule prevents Korean "Close" text in tooltip from wrapping. See https://github.com/microsoft/aspire/issues/8170 */ fluent-tooltip[anchor="dialog_close"] > div { word-break: keep-all; diff --git a/src/Aspire.Hosting.Analyzers/Infrastructure/WellKnownTypeData.cs b/src/Aspire.Hosting.Analyzers/Infrastructure/WellKnownTypeData.cs index dbf8b3da47d..44770f9677e 100644 --- a/src/Aspire.Hosting.Analyzers/Infrastructure/WellKnownTypeData.cs +++ b/src/Aspire.Hosting.Analyzers/Infrastructure/WellKnownTypeData.cs @@ -19,6 +19,8 @@ public enum WellKnownType Aspire_Hosting_IDistributedApplicationBuilder, System_Threading_Tasks_Task, System_Threading_Tasks_Task_1, + System_Threading_Tasks_ValueTask, + System_Threading_Tasks_ValueTask_1, // Date/time and scalar types System_DateTimeOffset, @@ -52,6 +54,8 @@ public enum WellKnownType "Aspire.Hosting.IDistributedApplicationBuilder", "System.Threading.Tasks.Task", "System.Threading.Tasks.Task`1", + "System.Threading.Tasks.ValueTask", + "System.Threading.Tasks.ValueTask`1", // Date/time and scalar types "System.DateTimeOffset", diff --git a/src/Aspire.Hosting.Azure.AppConfiguration/AzureAppConfigurationResource.cs b/src/Aspire.Hosting.Azure.AppConfiguration/AzureAppConfigurationResource.cs index b3e11a7c663..6db2b8a2291 100644 --- a/src/Aspire.Hosting.Azure.AppConfiguration/AzureAppConfigurationResource.cs +++ b/src/Aspire.Hosting.Azure.AppConfiguration/AzureAppConfigurationResource.cs @@ -77,8 +77,6 @@ public override ProvisionableResource AddAsExistingResource(AzureResourceInfrast return store; } - BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; - IEnumerable IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["configurationStores"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.azconfig.io"; diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs index 35ef66f13c7..0ccf61bc58b 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs @@ -263,8 +263,6 @@ IEnumerable> IResourceWithConnectionSt } } - BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; - IEnumerable IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["Sql"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.documents.azure.com"; diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs index d41cf08fe33..717f7818fbc 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs +++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs @@ -213,8 +213,6 @@ IEnumerable> IResourceWithConnectionSt } } - BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; - IEnumerable IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["namespace"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.servicebus.windows.net"; diff --git a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs index c070dd76ba0..e1371df42f2 100644 --- a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs +++ b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs @@ -147,8 +147,6 @@ IEnumerable> IResourceWithConnectionSt yield return new("Uri", UriExpression); } - BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; - IEnumerable IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["vault"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.vaultcore.azure.net"; diff --git a/src/Aspire.Hosting.Azure.Kusto/AzureKustoEmulatorContainerImageTags.cs b/src/Aspire.Hosting.Azure.Kusto/AzureKustoEmulatorContainerImageTags.cs index 3a6cf44fec9..2b9f43ed83e 100644 --- a/src/Aspire.Hosting.Azure.Kusto/AzureKustoEmulatorContainerImageTags.cs +++ b/src/Aspire.Hosting.Azure.Kusto/AzureKustoEmulatorContainerImageTags.cs @@ -23,6 +23,6 @@ internal static class AzureKustoEmulatorContainerImageTags /// /// The tag for the Kusto emulator container image. /// - /// latest - public static string Tag { get; } = "latest"; + /// 2026.03.16.1116-2611-994a3c9-master + public static string Tag { get; } = "2026.03.16.1116-2611-994a3c9-master"; } diff --git a/src/Aspire.Hosting.Azure.Network/AzurePrivateEndpointExtensions.cs b/src/Aspire.Hosting.Azure.Network/AzurePrivateEndpointExtensions.cs index 2d6fa2bf668..afd5bbd2ca2 100644 --- a/src/Aspire.Hosting.Azure.Network/AzurePrivateEndpointExtensions.cs +++ b/src/Aspire.Hosting.Azure.Network/AzurePrivateEndpointExtensions.cs @@ -3,7 +3,6 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; -using Aspire.Hosting.Azure.Network; using Azure.Provisioning; using Azure.Provisioning.Network; using Azure.Provisioning.PrivateDns; diff --git a/src/Aspire.Hosting.Azure.Network/IAzurePrivateEndpointTargetNotification.cs b/src/Aspire.Hosting.Azure.Network/IAzurePrivateEndpointTargetNotification.cs index aa87e2c1b11..47c7a3a1c96 100644 --- a/src/Aspire.Hosting.Azure.Network/IAzurePrivateEndpointTargetNotification.cs +++ b/src/Aspire.Hosting.Azure.Network/IAzurePrivateEndpointTargetNotification.cs @@ -3,7 +3,7 @@ using Aspire.Hosting.ApplicationModel; -namespace Aspire.Hosting.Azure.Network; +namespace Aspire.Hosting.Azure; /// /// An optional interface that can be implemented by resources that are targets for diff --git a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs index ee53b338bb7..795f822b45a 100644 --- a/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs +++ b/src/Aspire.Hosting.Azure.PostgreSQL/AzurePostgresFlexibleServerResource.cs @@ -314,8 +314,6 @@ IEnumerable> IResourceWithConnectionSt return propertiesDictionary; } - BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; - IEnumerable IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["postgresqlServer"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.postgres.database.azure.com"; diff --git a/src/Aspire.Hosting.Azure.Redis/AzureManagedRedisResource.cs b/src/Aspire.Hosting.Azure.Redis/AzureManagedRedisResource.cs index 65f40b36bfc..3d7d92d3fc1 100644 --- a/src/Aspire.Hosting.Azure.Redis/AzureManagedRedisResource.cs +++ b/src/Aspire.Hosting.Azure.Redis/AzureManagedRedisResource.cs @@ -255,8 +255,6 @@ IEnumerable> IResourceWithConnectionSt } } - BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; - IEnumerable IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["redisEnterprise"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.redis.azure.net"; diff --git a/src/Aspire.Hosting.Azure.Search/AzureSearchResource.cs b/src/Aspire.Hosting.Azure.Search/AzureSearchResource.cs index b607946d586..01c1d91615c 100644 --- a/src/Aspire.Hosting.Azure.Search/AzureSearchResource.cs +++ b/src/Aspire.Hosting.Azure.Search/AzureSearchResource.cs @@ -91,8 +91,6 @@ IEnumerable> IResourceWithConnectionSt yield return new("Uri", UriExpression); } - BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; - IEnumerable IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["searchService"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.search.windows.net"; diff --git a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusResource.cs b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusResource.cs index 332feb78b66..b7549ecca2c 100644 --- a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusResource.cs +++ b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusResource.cs @@ -193,8 +193,6 @@ IEnumerable> IResourceWithConnectionSt } } - BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; - IEnumerable IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["namespace"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.servicebus.windows.net"; diff --git a/src/Aspire.Hosting.Azure.SignalR/AzureSignalRResource.cs b/src/Aspire.Hosting.Azure.SignalR/AzureSignalRResource.cs index a38431efde0..bba2b89609f 100644 --- a/src/Aspire.Hosting.Azure.SignalR/AzureSignalRResource.cs +++ b/src/Aspire.Hosting.Azure.SignalR/AzureSignalRResource.cs @@ -94,8 +94,6 @@ IEnumerable> IResourceWithConnectionSt yield return new("Uri", UriExpression); } - BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; - IEnumerable IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["signalr"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.service.signalr.net"; diff --git a/src/Aspire.Hosting.Azure.Sql/AzureSqlServerResource.cs b/src/Aspire.Hosting.Azure.Sql/AzureSqlServerResource.cs index 592961f28d0..5c01b66d442 100644 --- a/src/Aspire.Hosting.Azure.Sql/AzureSqlServerResource.cs +++ b/src/Aspire.Hosting.Azure.Sql/AzureSqlServerResource.cs @@ -7,7 +7,6 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using Aspire.Hosting.ApplicationModel; -using Aspire.Hosting.Azure.Network; using Aspire.Hosting.Eventing; using Aspire.Hosting.Pipelines; using Azure.Provisioning; @@ -439,8 +438,6 @@ IEnumerable> IResourceWithConnectionSt return result; } - BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; - IEnumerable IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["sqlServer"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.database.windows.net"; diff --git a/src/Aspire.Hosting.Azure.WebPubSub/AzureWebPubSubResource.cs b/src/Aspire.Hosting.Azure.WebPubSub/AzureWebPubSubResource.cs index 6869a8b0cc6..a60391fae13 100644 --- a/src/Aspire.Hosting.Azure.WebPubSub/AzureWebPubSubResource.cs +++ b/src/Aspire.Hosting.Azure.WebPubSub/AzureWebPubSubResource.cs @@ -81,8 +81,6 @@ IEnumerable> IResourceWithConnectionSt yield return new("Uri", UriExpression); } - BicepOutputReference IAzurePrivateEndpointTarget.Id => Id; - IEnumerable IAzurePrivateEndpointTarget.GetPrivateLinkGroupIds() => ["webpubsub"]; string IAzurePrivateEndpointTarget.GetPrivateDnsZoneName() => "privatelink.webpubsub.azure.com"; diff --git a/src/Aspire.Hosting.CodeGeneration.Go/AssemblyInfo.cs b/src/Aspire.Hosting.CodeGeneration.Go/AssemblyInfo.cs new file mode 100644 index 00000000000..79236a7dbec --- /dev/null +++ b/src/Aspire.Hosting.CodeGeneration.Go/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +[assembly: Experimental("ASPIREATS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] diff --git a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs index 00e2fd1e658..29ea4501cc6 100644 --- a/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Go/AtsGoCodeGenerator.cs @@ -12,7 +12,7 @@ namespace Aspire.Hosting.CodeGeneration.Go; /// Generates a Go SDK using the ATS (Aspire Type System) capability-based API. /// Produces wrapper structs that proxy capabilities via JSON-RPC. /// -public sealed class AtsGoCodeGenerator : ICodeGenerator +internal sealed class AtsGoCodeGenerator : ICodeGenerator { private static readonly HashSet s_goKeywords = new(StringComparer.Ordinal) { @@ -492,6 +492,13 @@ private void GenerateConnectionHelpers() WriteLine("\tif err := client.Connect(); err != nil {"); WriteLine("\t\treturn nil, err"); WriteLine("\t}"); + WriteLine("\tauthToken := os.Getenv(\"ASPIRE_REMOTE_APPHOST_TOKEN\")"); + WriteLine("\tif authToken == \"\" {"); + WriteLine("\t\treturn nil, fmt.Errorf(\"ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`\")"); + WriteLine("\t}"); + WriteLine("\tif err := client.Authenticate(authToken); err != nil {"); + WriteLine("\t\treturn nil, err"); + WriteLine("\t}"); WriteLine("\tclient.OnDisconnect(func() { os.Exit(1) })"); WriteLine("\treturn client, nil"); WriteLine("}"); diff --git a/src/Aspire.Hosting.CodeGeneration.Go/GoLanguageSupport.cs b/src/Aspire.Hosting.CodeGeneration.Go/GoLanguageSupport.cs index 99b21fdce31..2b6b637ee81 100644 --- a/src/Aspire.Hosting.CodeGeneration.Go/GoLanguageSupport.cs +++ b/src/Aspire.Hosting.CodeGeneration.Go/GoLanguageSupport.cs @@ -9,7 +9,7 @@ namespace Aspire.Hosting.CodeGeneration.Go; /// Provides language support for Go AppHosts. /// Implements scaffolding, detection, and runtime configuration. /// -public sealed class GoLanguageSupport : ILanguageSupport +internal sealed class GoLanguageSupport : ILanguageSupport { /// /// The language/runtime identifier for Go. diff --git a/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go b/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go index 79e8c8c5d27..9bceaa57181 100644 --- a/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go +++ b/src/Aspire.Hosting.CodeGeneration.Go/Resources/transport.go @@ -268,6 +268,21 @@ func (c *AspireClient) InvokeCapability(capabilityID string, args map[string]any return WrapIfHandle(result, c), nil } +// Authenticate authenticates the client session with the AppHost server. +func (c *AspireClient) Authenticate(token string) error { + result, err := c.sendRequest("authenticate", []any{token}) + if err != nil { + return err + } + + authenticated, _ := result.(bool) + if !authenticated { + return errors.New("failed to authenticate to the AppHost server") + } + + return nil +} + // CancelToken cancels a cancellation token on the server. func (c *AspireClient) CancelToken(tokenID string) bool { result, err := c.sendRequest("cancelToken", []any{tokenID}) diff --git a/src/Aspire.Hosting.CodeGeneration.Java/AssemblyInfo.cs b/src/Aspire.Hosting.CodeGeneration.Java/AssemblyInfo.cs new file mode 100644 index 00000000000..79236a7dbec --- /dev/null +++ b/src/Aspire.Hosting.CodeGeneration.Java/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +[assembly: Experimental("ASPIREATS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] diff --git a/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs index a117af08ff5..1a96c4760e2 100644 --- a/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Java/AtsJavaCodeGenerator.cs @@ -12,7 +12,7 @@ namespace Aspire.Hosting.CodeGeneration.Java; /// Generates a Java SDK using the ATS (Aspire Type System) capability-based API. /// Produces wrapper classes that proxy capabilities via JSON-RPC. /// -public sealed class AtsJavaCodeGenerator : ICodeGenerator +internal sealed class AtsJavaCodeGenerator : ICodeGenerator { private static readonly HashSet s_javaKeywords = new(StringComparer.Ordinal) { @@ -469,6 +469,11 @@ private void GenerateConnectionHelpers() WriteLine(" }"); WriteLine(" AspireClient client = new AspireClient(socketPath);"); WriteLine(" client.connect();"); + WriteLine(" String authToken = System.getenv(\"ASPIRE_REMOTE_APPHOST_TOKEN\");"); + WriteLine(" if (authToken == null || authToken.isEmpty()) {"); + WriteLine(" throw new RuntimeException(\"ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`.\");"); + WriteLine(" }"); + WriteLine(" client.authenticate(authToken);"); WriteLine(" client.onDisconnect(() -> System.exit(1));"); WriteLine(" return client;"); WriteLine(" }"); diff --git a/src/Aspire.Hosting.CodeGeneration.Java/JavaLanguageSupport.cs b/src/Aspire.Hosting.CodeGeneration.Java/JavaLanguageSupport.cs index d050f07fc46..2db8a48fc01 100644 --- a/src/Aspire.Hosting.CodeGeneration.Java/JavaLanguageSupport.cs +++ b/src/Aspire.Hosting.CodeGeneration.Java/JavaLanguageSupport.cs @@ -9,7 +9,7 @@ namespace Aspire.Hosting.CodeGeneration.Java; /// Provides language support for Java AppHosts. /// Implements scaffolding, detection, and runtime configuration. /// -public sealed class JavaLanguageSupport : ILanguageSupport +internal sealed class JavaLanguageSupport : ILanguageSupport { /// /// The language/runtime identifier for Java. diff --git a/src/Aspire.Hosting.CodeGeneration.Java/Resources/Transport.java b/src/Aspire.Hosting.CodeGeneration.Java/Resources/Transport.java index cef9f1cefd9..8848ded3137 100644 --- a/src/Aspire.Hosting.CodeGeneration.Java/Resources/Transport.java +++ b/src/Aspire.Hosting.CodeGeneration.Java/Resources/Transport.java @@ -181,6 +181,31 @@ public Object invokeCapability(String capabilityId, Map args) { } } + public void authenticate(String token) { + int id = requestId.incrementAndGet(); + + List params = List.of(token); + + Map request = new HashMap<>(); + request.put("jsonrpc", "2.0"); + request.put("id", id); + request.put("method", "authenticate"); + request.put("params", params); + + debug("Sending request authenticate with id=" + id); + + try { + sendMessage(request); + Object result = readResponse(id); + if (!(result instanceof Boolean authenticated) || !authenticated) { + throw new RuntimeException("Failed to authenticate to the AppHost server."); + } + } catch (IOException e) { + handleDisconnect(); + throw new RuntimeException("Failed to authenticate: " + e.getMessage(), e); + } + } + private void sendMessage(Map message) throws IOException { String json = toJson(message); byte[] content = json.getBytes(StandardCharsets.UTF_8); diff --git a/src/Aspire.Hosting.CodeGeneration.Python/AssemblyInfo.cs b/src/Aspire.Hosting.CodeGeneration.Python/AssemblyInfo.cs new file mode 100644 index 00000000000..79236a7dbec --- /dev/null +++ b/src/Aspire.Hosting.CodeGeneration.Python/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +[assembly: Experimental("ASPIREATS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] diff --git a/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs index fd4db795ff2..173a3be126d 100644 --- a/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Python/AtsPythonCodeGenerator.cs @@ -13,7 +13,7 @@ namespace Aspire.Hosting.CodeGeneration.Python; /// Generates a Python SDK using the ATS (Aspire Type System) capability-based API. /// Produces wrapper classes that proxy capabilities via JSON-RPC. /// -public sealed class AtsPythonCodeGenerator : ICodeGenerator +internal sealed class AtsPythonCodeGenerator : ICodeGenerator { private static readonly HashSet s_pythonKeywords = new(StringComparer.OrdinalIgnoreCase) { @@ -384,6 +384,10 @@ private void GenerateConnectionHelpers() WriteLine(" raise RuntimeError(\"REMOTE_APP_HOST_SOCKET_PATH environment variable not set. Run this application using `aspire run`.\")"); WriteLine(" client = AspireClient(socket_path)"); WriteLine(" client.connect()"); + WriteLine(" auth_token = os.environ.get(\"ASPIRE_REMOTE_APPHOST_TOKEN\")"); + WriteLine(" if not auth_token:"); + WriteLine(" raise RuntimeError(\"ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`.\")"); + WriteLine(" client.authenticate(auth_token)"); WriteLine(" client.on_disconnect(lambda: sys.exit(1))"); WriteLine(" return client"); WriteLine(); diff --git a/src/Aspire.Hosting.CodeGeneration.Python/PythonLanguageSupport.cs b/src/Aspire.Hosting.CodeGeneration.Python/PythonLanguageSupport.cs index f9f5ccea604..2b39ca57771 100644 --- a/src/Aspire.Hosting.CodeGeneration.Python/PythonLanguageSupport.cs +++ b/src/Aspire.Hosting.CodeGeneration.Python/PythonLanguageSupport.cs @@ -10,7 +10,7 @@ namespace Aspire.Hosting.CodeGeneration.Python; /// Provides language support for Python AppHosts. /// Implements scaffolding, detection, and runtime configuration. /// -public sealed class PythonLanguageSupport : ILanguageSupport +internal sealed class PythonLanguageSupport : ILanguageSupport { /// /// The language/runtime identifier for Python. diff --git a/src/Aspire.Hosting.CodeGeneration.Python/Resources/transport.py b/src/Aspire.Hosting.CodeGeneration.Python/Resources/transport.py index a06ca8d6e23..de4b2ddef2a 100644 --- a/src/Aspire.Hosting.CodeGeneration.Python/Resources/transport.py +++ b/src/Aspire.Hosting.CodeGeneration.Python/Resources/transport.py @@ -154,6 +154,10 @@ def invoke_capability(self, capability_id: str, args: Optional[Dict[str, Any]] = raise CapabilityError(result["$error"]) return wrap_if_handle(result, self) + def authenticate(self, token: str) -> None: + if not bool(self._send_request("authenticate", [token])): + raise RuntimeError("Failed to authenticate to the AppHost server.") + def cancel_token(self, token_id: str) -> bool: return bool(self._send_request("cancelToken", [token_id])) diff --git a/src/Aspire.Hosting.CodeGeneration.Rust/AssemblyInfo.cs b/src/Aspire.Hosting.CodeGeneration.Rust/AssemblyInfo.cs new file mode 100644 index 00000000000..79236a7dbec --- /dev/null +++ b/src/Aspire.Hosting.CodeGeneration.Rust/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +[assembly: Experimental("ASPIREATS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] diff --git a/src/Aspire.Hosting.CodeGeneration.Rust/AtsRustCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.Rust/AtsRustCodeGenerator.cs index 8d951576272..33ea6c05f6e 100644 --- a/src/Aspire.Hosting.CodeGeneration.Rust/AtsRustCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.Rust/AtsRustCodeGenerator.cs @@ -13,7 +13,7 @@ namespace Aspire.Hosting.CodeGeneration.Rust; /// Generates a Rust SDK using the ATS (Aspire Type System) capability-based API. /// Produces wrapper structs that proxy capabilities via JSON-RPC. /// -public sealed class AtsRustCodeGenerator : ICodeGenerator +internal sealed class AtsRustCodeGenerator : ICodeGenerator { private static readonly HashSet s_rustKeywords = new(StringComparer.Ordinal) { @@ -573,6 +573,9 @@ private void GenerateConnectionHelpers() WriteLine(" .map_err(|_| \"REMOTE_APP_HOST_SOCKET_PATH environment variable not set. Run this application using `aspire run`\")?;"); WriteLine(" let client = Arc::new(AspireClient::new(&socket_path));"); WriteLine(" client.connect()?;"); + WriteLine(" let auth_token = std::env::var(\"ASPIRE_REMOTE_APPHOST_TOKEN\")"); + WriteLine(" .map_err(|_| \"ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`\")?;"); + WriteLine(" client.authenticate(&auth_token)?;"); WriteLine(" Ok(client)"); WriteLine("}"); WriteLine(); diff --git a/src/Aspire.Hosting.CodeGeneration.Rust/Resources/transport.rs b/src/Aspire.Hosting.CodeGeneration.Rust/Resources/transport.rs index 14a28068766..48acf677c35 100644 --- a/src/Aspire.Hosting.CodeGeneration.Rust/Resources/transport.rs +++ b/src/Aspire.Hosting.CodeGeneration.Rust/Resources/transport.rs @@ -307,6 +307,16 @@ impl AspireClient { Ok(wrap_if_handle(result, None)) } + /// Authenticates the client session with the AppHost server. + pub fn authenticate(&self, token: &str) -> Result<(), Box> { + let result = self.send_request("authenticate", json!([token]))?; + if result.as_bool().unwrap_or(false) { + return Ok(()); + } + + Err("Failed to authenticate to the AppHost server.".into()) + } + /// Cancels a cancellation token on the server. pub fn cancel_token(&self, token_id: &str) -> Result> { let result = self.send_request("cancelToken", json!([token_id]))?; diff --git a/src/Aspire.Hosting.CodeGeneration.Rust/RustLanguageSupport.cs b/src/Aspire.Hosting.CodeGeneration.Rust/RustLanguageSupport.cs index 69455ca1f5b..a8278e4f70e 100644 --- a/src/Aspire.Hosting.CodeGeneration.Rust/RustLanguageSupport.cs +++ b/src/Aspire.Hosting.CodeGeneration.Rust/RustLanguageSupport.cs @@ -9,7 +9,7 @@ namespace Aspire.Hosting.CodeGeneration.Rust; /// Provides language support for Rust AppHosts. /// Implements scaffolding, detection, and runtime configuration. /// -public sealed class RustLanguageSupport : ILanguageSupport +internal sealed class RustLanguageSupport : ILanguageSupport { /// /// The language/runtime identifier for Rust. diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/AssemblyInfo.cs b/src/Aspire.Hosting.CodeGeneration.TypeScript/AssemblyInfo.cs new file mode 100644 index 00000000000..79236a7dbec --- /dev/null +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; + +[assembly: Experimental("ASPIREATS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs b/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs index 8d2b152a7e7..66d6da9ed58 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs @@ -96,7 +96,7 @@ internal sealed class BuilderModel /// /// /// -public sealed class AtsTypeScriptCodeGenerator : ICodeGenerator +internal sealed class AtsTypeScriptCodeGenerator : ICodeGenerator { private TextWriter _writer = null!; diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/transport.ts b/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/transport.ts index 904e57142f9..6d29cf289d9 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/transport.ts +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/transport.ts @@ -830,7 +830,7 @@ export class AspireClient { failConnect(new Error('Connection closed before JSON-RPC was established')); }; - const onConnect = () => { + const onConnect = async () => { if (settled) { return; } @@ -867,7 +867,16 @@ export class AspireClient { socket.on('error', onConnectedSocketError); socket.on('close', onConnectedSocketClose); + const authToken = process.env.ASPIRE_REMOTE_APPHOST_TOKEN; + if (!authToken) { + throw new Error('ASPIRE_REMOTE_APPHOST_TOKEN environment variable is not set.'); + } this.connection.listen(); + const authenticated = await this.connection.sendRequest('authenticate', authToken); + if (!authenticated) { + throw new Error('Failed to authenticate to the AppHost server.'); + } + connectedClients.add(this); this._connectPromise = null; settled = true; diff --git a/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs b/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs index cfbe6abec09..d858ad540af 100644 --- a/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs +++ b/src/Aspire.Hosting.CodeGeneration.TypeScript/TypeScriptLanguageSupport.cs @@ -9,7 +9,7 @@ namespace Aspire.Hosting.CodeGeneration.TypeScript; /// Provides language support for TypeScript AppHosts. /// Implements scaffolding, detection, and runtime configuration. /// -public sealed class TypeScriptLanguageSupport : ILanguageSupport +internal sealed class TypeScriptLanguageSupport : ILanguageSupport { /// /// The language/runtime identifier for TypeScript with Node.js. diff --git a/src/Aspire.Hosting.Docker/Aspire.Hosting.Docker.csproj b/src/Aspire.Hosting.Docker/Aspire.Hosting.Docker.csproj index 91f3ef9c2c9..805f94abde4 100644 --- a/src/Aspire.Hosting.Docker/Aspire.Hosting.Docker.csproj +++ b/src/Aspire.Hosting.Docker/Aspire.Hosting.Docker.csproj @@ -3,7 +3,7 @@ $(DefaultTargetFramework) true - true + true aspire hosting docker docker-compose Docker Compose publishing for Aspire. true diff --git a/src/Aspire.Hosting.Foundry/FoundryExtensions.cs b/src/Aspire.Hosting.Foundry/FoundryExtensions.cs index 7a743712494..ddd6f2e8893 100644 --- a/src/Aspire.Hosting.Foundry/FoundryExtensions.cs +++ b/src/Aspire.Hosting.Foundry/FoundryExtensions.cs @@ -23,6 +23,7 @@ namespace Aspire.Hosting; public static class FoundryExtensions { private const string DefaultCapabilityHostName = "foundry-caphost"; + internal const string LocalProjectsNotSupportedMessage = "Microsoft Foundry projects are not supported when the parent Foundry resource is configured with RunAsFoundryLocal()."; /// /// Adds a Microsoft Foundry resource to the application model. @@ -140,6 +141,7 @@ public static IResourceBuilder RunAsFoundryLocal(this IResource } var resource = builder.Resource; + ThrowIfProjectsConfiguredForLocal(builder, resource); resource.Annotations.Add(new EmulatorResourceAnnotation()); builder.ApplicationBuilder.Services.AddSingleton(); @@ -169,6 +171,16 @@ public static IResourceBuilder RunAsFoundryLocal(this IResource return builder; } + internal static void ThrowIfProjectsConfiguredForLocal(IResourceBuilder builder, FoundryResource resource) + { + if (builder.ApplicationBuilder.Resources + .OfType() + .Any(project => ReferenceEquals(project.Parent, resource))) + { + throw new InvalidOperationException(LocalProjectsNotSupportedMessage); + } + } + /// /// Assigns the specified roles to the given resource, granting it the necessary permissions /// on the target Microsoft Foundry resource. This replaces the default role assignments for the resource. diff --git a/src/Aspire.Hosting.Foundry/FoundryModel.Generated.cs b/src/Aspire.Hosting.Foundry/FoundryModel.Generated.cs index 5c1d87d32a7..9ee5890a0fb 100644 --- a/src/Aspire.Hosting.Foundry/FoundryModel.Generated.cs +++ b/src/Aspire.Hosting.Foundry/FoundryModel.Generated.cs @@ -44,7 +44,7 @@ public static partial class Anthropic public static readonly FoundryModel ClaudeOpus45 = new() { Name = "claude-opus-4-5", Version = "20251101", Format = "Anthropic" }; /// - /// Claude Opus 4.6 is the latest version of Anthropic's most intelligent model, and the world's best model for coding, enterprise agents, and professional work. With a 1M token context window (beta) and 128K max output, Opus 4.6 is ideal for production code, + /// Claude Opus 4.6 is the latest version of Anthropic's most intelligent model, and the world's best model for coding, enterprise agents, and professional work. With a 1M token context window and 128K max output, Opus 4.6 is ideal for production code, sophist /// public static readonly FoundryModel ClaudeOpus46 = new() { Name = "claude-opus-4-6", Version = "1", Format = "Anthropic" }; @@ -54,7 +54,7 @@ public static partial class Anthropic public static readonly FoundryModel ClaudeSonnet45 = new() { Name = "claude-sonnet-4-5", Version = "20250929", Format = "Anthropic" }; /// - /// Claude Sonnet 4.6 delivers frontier intelligence at scale—built for coding, agents, and enterprise workflows. With a 1M token context window (beta) and 128K max output, Sonnet 4.6 is ideal for coding, agents, office tasks, financial analysis, cybersecurity + /// Claude Sonnet 4.6 delivers frontier intelligence at scale—built for coding, agents, and enterprise workflows. With a 1M token context window and 128K max output, Sonnet 4.6 is ideal for coding, agents, office tasks, financial analysis, cybersecurity, and c /// public static readonly FoundryModel ClaudeSonnet46 = new() { Name = "claude-sonnet-4-6", Version = "1", Format = "Anthropic" }; } @@ -2785,6 +2785,16 @@ public static partial class OpenAI /// public static readonly FoundryModel Gpt54 = new() { Name = "gpt-5.4", Version = "2026-03-05", Format = "OpenAI" }; + /// + /// GPT‑5.4‑mini is a compact, cost‑efficient model designed for reliable performance across high‑volume, everyday AI workloads. + /// + public static readonly FoundryModel Gpt54Mini = new() { Name = "gpt-5.4-mini", Version = "2026-03-17", Format = "OpenAI" }; + + /// + /// GPT‑5.4‑nano is a lightweight, ultra‑efficient model designed for low‑latency, cost‑effective tasks at massive scale. + /// + public static readonly FoundryModel Gpt54Nano = new() { Name = "gpt-5.4-nano", Version = "2026-03-17", Format = "OpenAI" }; + /// /// GPT‑5.4-Pro is OpenAI’s most capable frontier model, built to deliver faster, more reliable results for complex professional work. /// diff --git a/src/Aspire.Hosting.Foundry/FoundryModel.Local.Generated.cs b/src/Aspire.Hosting.Foundry/FoundryModel.Local.Generated.cs index 23b47cb2b35..d9e508d0a52 100644 --- a/src/Aspire.Hosting.Foundry/FoundryModel.Local.Generated.cs +++ b/src/Aspire.Hosting.Foundry/FoundryModel.Local.Generated.cs @@ -896,7 +896,95 @@ public static partial class Local /// /// See Hugging Face model Qwen3-0.6B for details. /// - public static readonly FoundryModel Qwen306b = new() { Name = "qwen3-0.6b", Version = "2", Format = "Microsoft" }; + public static readonly FoundryModel Qwen306b = new() { Name = "qwen3-0.6b", Version = "1", Format = "Microsoft" }; + + /// + /// This model is an optimized version of Qwen3-1.7B to enable local inference. This model uses KLD Gradient quantization. + /// + /// Model Description + /// + /// + /// + /// + /// + /// Developed by: Microsoft + /// + /// + /// + /// + /// + /// Model type: ONNX + /// + /// + /// + /// + /// + /// License: apache-2.0 + /// + /// + /// + /// + /// + /// Model Description: This is a conversion of the Qwen3-1.7B for local inference. + /// + /// + /// + /// + /// + /// Disclaimer: Model is only an optimization of the base model, any risk associated with the model is the responsibility of the user of the model. Please verify and test for your scenarios. There may be a slight difference in output from the base model with the optimizations applied. Note that optimizations applied are distinct from fine tuning and thus do not alter the intended uses or capabilities of the model. + /// + /// + /// + /// + /// Base Model Information + /// + /// See Hugging Face model Qwen3-1.7B for details. + /// + public static readonly FoundryModel Qwen317b = new() { Name = "qwen3-1.7b", Version = "1", Format = "Microsoft" }; + + /// + /// This model is an optimized version of Qwen3-14B to enable local inference. This model uses GPTQ quantization. + /// + /// Model Description + /// + /// + /// + /// + /// + /// Developed by: Microsoft + /// + /// + /// + /// + /// + /// Model type: ONNX + /// + /// + /// + /// + /// + /// License: apache-2.0 + /// + /// + /// + /// + /// + /// Model Description: This is a conversion of the Qwen3-14B for local inference. + /// + /// + /// + /// + /// + /// Disclaimer: Model is only an optimization of the base model, any risk associated with the model is the responsibility of the user of the model. Please verify and test for your scenarios. There may be a slight difference in output from the base model with the optimizations applied. Note that optimizations applied are distinct from fine tuning and thus do not alter the intended uses or capabilities of the model. + /// + /// + /// + /// + /// Base Model Information + /// + /// See Hugging Face model Qwen3-14B for details. + /// + public static readonly FoundryModel Qwen314b = new() { Name = "qwen3-14b", Version = "1", Format = "Microsoft" }; /// /// This model is an optimized version of Qwen3-4B to enable local inference. This model uses KLD Gradient quantization. @@ -942,6 +1030,50 @@ public static partial class Local /// public static readonly FoundryModel Qwen34b = new() { Name = "qwen3-4b", Version = "1", Format = "Microsoft" }; + /// + /// This model is an optimized version of Qwen3-8B to enable local inference. This model uses KLD Gradient quantization. + /// + /// Model Description + /// + /// + /// + /// + /// + /// Developed by: Microsoft + /// + /// + /// + /// + /// + /// Model type: ONNX + /// + /// + /// + /// + /// + /// License: apache-2.0 + /// + /// + /// + /// + /// + /// Model Description: This is a conversion of the Qwen3-8B for local inference. + /// + /// + /// + /// + /// + /// Disclaimer: Model is only an optimization of the base model, any risk associated with the model is the responsibility of the user of the model. Please verify and test for your scenarios. There may be a slight difference in output from the base model with the optimizations applied. Note that optimizations applied are distinct from fine tuning and thus do not alter the intended uses or capabilities of the model. + /// + /// + /// + /// + /// Base Model Information + /// + /// See Hugging Face model Qwen3-8B for details. + /// + public static readonly FoundryModel Qwen38b = new() { Name = "qwen3-8b", Version = "1", Format = "Microsoft" }; + /// /// This model is an optimized version of Qwen3-VL-2B-Instruct to enable local inference. This model uses RTN quantization. /// diff --git a/src/Aspire.Hosting.Foundry/Project/ProjectBuilderExtension.cs b/src/Aspire.Hosting.Foundry/Project/ProjectBuilderExtension.cs index bc84e4d83ed..7a4ad61dcae 100644 --- a/src/Aspire.Hosting.Foundry/Project/ProjectBuilderExtension.cs +++ b/src/Aspire.Hosting.Foundry/Project/ProjectBuilderExtension.cs @@ -41,6 +41,10 @@ public static IResourceBuilder AddProject { ArgumentNullException.ThrowIfNull(builder); ArgumentException.ThrowIfNullOrEmpty(name); + if (builder.Resource.IsEmulator) + { + throw new InvalidOperationException(FoundryExtensions.LocalProjectsNotSupportedMessage); + } builder.ApplicationBuilder.Services.Configure(o => o.SupportsTargetedRoleAssignments = true); @@ -598,10 +602,7 @@ static void configureInfrastructure(AzureResourceInfrastructure infrastructure) } var resource = new AzureContainerRegistryResource(name, configureInfrastructure); - if (builder.ExecutionContext.IsPublishMode) - { - builder.AddResource(resource); - } + builder.AddResource(resource); return resource; } } diff --git a/src/Aspire.Hosting.Integration.Analyzers/AspireExportAnalyzer.cs b/src/Aspire.Hosting.Integration.Analyzers/AspireExportAnalyzer.cs index eec5cb99ba8..cd857a02910 100644 --- a/src/Aspire.Hosting.Integration.Analyzers/AspireExportAnalyzer.cs +++ b/src/Aspire.Hosting.Integration.Analyzers/AspireExportAnalyzer.cs @@ -48,6 +48,8 @@ private void AnalyzeCompilationStart(CompilationStartAnalysisContext context) return; } + var currentAssemblyExportedTypes = GetAssemblyExportedTypes(context.Compilation.Assembly, aspireExportAttribute); + // Try to get AspireExportIgnoreAttribute for ASPIREEXPORT008 INamedTypeSymbol? aspireExportIgnoreAttribute = null; try @@ -75,7 +77,7 @@ private void AnalyzeCompilationStart(CompilationStartAnalysisContext context) var exportsByKey = new ConcurrentDictionary<(string ExportId, string TargetType), ConcurrentBag<(IMethodSymbol Method, Location Location)>>(); context.RegisterSymbolAction( - c => AnalyzeMethod(c, wellKnownTypes, aspireExportAttribute, aspireExportIgnoreAttribute, aspireUnionAttribute, exportsByKey), + c => AnalyzeMethod(c, wellKnownTypes, aspireExportAttribute, aspireExportIgnoreAttribute, aspireUnionAttribute, currentAssemblyExportedTypes, exportsByKey), SymbolKind.Method); // At the end of compilation, report duplicate export IDs @@ -120,6 +122,7 @@ private static void AnalyzeMethod( INamedTypeSymbol aspireExportAttribute, INamedTypeSymbol? aspireExportIgnoreAttribute, INamedTypeSymbol? aspireUnionAttribute, + HashSet currentAssemblyExportedTypes, ConcurrentDictionary<(string ExportId, string TargetType), ConcurrentBag<(IMethodSymbol Method, Location Location)>> exportsByKey) { var method = (IMethodSymbol)context.Symbol; @@ -145,10 +148,23 @@ private static void AnalyzeMethod( } } + var containingTypeHasExportIgnore = false; + if (!hasExportIgnore && aspireExportIgnoreAttribute is not null) + { + foreach (var attr in method.ContainingType.GetAttributes()) + { + if (SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportIgnoreAttribute)) + { + containingTypeHasExportIgnore = true; + break; + } + } + } + // ASPIREEXPORT008: Check for missing export attributes on builder extension methods - if (exportAttribute is null && !hasExportIgnore && !isObsolete) + if (exportAttribute is null && !hasExportIgnore && !containingTypeHasExportIgnore && !isObsolete) { - AnalyzeMissingExportAttribute(context, method, wellKnownTypes, aspireExportAttribute); + AnalyzeMissingExportAttribute(context, method, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); } if (exportAttribute is null) @@ -158,9 +174,10 @@ private static void AnalyzeMethod( var attributeSyntax = exportAttribute.ApplicationSyntaxReference?.GetSyntax(context.CancellationToken); var location = attributeSyntax?.GetLocation() ?? method.Locations.FirstOrDefault() ?? Location.None; + var containingTypeExportAttribute = GetContainingTypeAspireExportAttribute(method.ContainingType, aspireExportAttribute); // Rule 1: Method must be static - if (!method.IsStatic) + if (!method.IsStatic && containingTypeExportAttribute is null) { context.ReportDiagnostic(Diagnostic.Create( Diagnostics.s_exportMethodMustBeStatic, @@ -179,7 +196,7 @@ private static void AnalyzeMethod( } // Rule 3: Validate return type is ATS-compatible - if (!IsAtsCompatibleType(method.ReturnType, wellKnownTypes, aspireExportAttribute)) + if (!IsAtsCompatibleType(method.ReturnType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes)) { context.ReportDiagnostic(Diagnostic.Create( Diagnostics.s_returnTypeMustBeAtsCompatible, @@ -191,7 +208,7 @@ private static void AnalyzeMethod( // Rule 4: Validate parameter types are ATS-compatible foreach (var parameter in method.Parameters) { - if (!IsAtsCompatibleParameter(parameter, wellKnownTypes, aspireExportAttribute)) + if (!IsAtsCompatibleParameter(parameter, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes)) { context.ReportDiagnostic(Diagnostic.Create( Diagnostics.s_parameterTypeMustBeAtsCompatible, @@ -204,7 +221,7 @@ private static void AnalyzeMethod( // Rule 5 (ASPIREEXPORT005/006): Validate [AspireUnion] on parameters if (aspireUnionAttribute is not null) { - AnalyzeUnionAttribute(context, parameter.GetAttributes(), aspireUnionAttribute, wellKnownTypes, aspireExportAttribute); + AnalyzeUnionAttribute(context, parameter.GetAttributes(), aspireUnionAttribute, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); } } @@ -229,7 +246,8 @@ private static void AnalyzeMissingExportAttribute( SymbolAnalysisContext context, IMethodSymbol method, WellKnownTypes wellKnownTypes, - INamedTypeSymbol aspireExportAttribute) + INamedTypeSymbol aspireExportAttribute, + HashSet currentAssemblyExportedTypes) { // Only check public static extension methods if (!method.IsStatic || !method.IsExtensionMethod || method.DeclaredAccessibility != Accessibility.Public) @@ -244,13 +262,13 @@ private static void AnalyzeMissingExportAttribute( // Only check methods extending exported handle types that participate in ATS. var firstParamType = method.Parameters[0].Type; - if (!RequiresExplicitExportCoverage(firstParamType, wellKnownTypes, aspireExportAttribute)) + if (!RequiresExplicitExportCoverage(firstParamType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes)) { return; } // Determine the incompatibility reason (if any) to include in the warning - var reason = GetIncompatibilityReason(method, wellKnownTypes, aspireExportAttribute); + var reason = GetIncompatibilityReason(method, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); var location = method.Locations.FirstOrDefault() ?? Location.None; context.ReportDiagnostic(Diagnostic.Create( @@ -505,8 +523,10 @@ private static bool IsOpenGenericResourceBuilder(ITypeSymbol type, WellKnownType return null; } - // Check that the type argument is a concrete type, not a type parameter - if (namedType.TypeArguments.Length == 1 && namedType.TypeArguments[0] is not ITypeParameterSymbol) + // Check that the type argument is a concrete resource type, not a type parameter or interface. + if (namedType.TypeArguments.Length == 1 && + namedType.TypeArguments[0] is not ITypeParameterSymbol && + namedType.TypeArguments[0].TypeKind is not TypeKind.Interface) { return namedType.TypeArguments[0].Name; } @@ -548,17 +568,19 @@ private static bool IsBuilderType(ITypeSymbol type, WellKnownTypes wellKnownType private static bool RequiresExplicitExportCoverage( ITypeSymbol type, WellKnownTypes wellKnownTypes, - INamedTypeSymbol aspireExportAttribute) + INamedTypeSymbol aspireExportAttribute, + HashSet currentAssemblyExportedTypes) { return IsBuilderType(type, wellKnownTypes) || IsResourceType(type, wellKnownTypes) || - HasAspireExportAttribute(type, aspireExportAttribute); + HasAspireExportAttribute(type, aspireExportAttribute, currentAssemblyExportedTypes); } private static string? GetIncompatibilityReason( IMethodSymbol method, WellKnownTypes wellKnownTypes, - INamedTypeSymbol aspireExportAttribute) + INamedTypeSymbol aspireExportAttribute, + HashSet currentAssemblyExportedTypes) { var reasons = new List(); @@ -611,7 +633,7 @@ private static bool RequiresExplicitExportCoverage( // Check delegate types more carefully if (IsDelegateType(paramType)) { - var reason = GetDelegateIncompatibilityReason(param, paramType, wellKnownTypes, aspireExportAttribute); + var reason = GetDelegateIncompatibilityReason(param, paramType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); if (reason is not null) { reasons.Add(reason); @@ -619,14 +641,14 @@ private static bool RequiresExplicitExportCoverage( continue; } - if (!IsAtsCompatibleValueType(paramType, wellKnownTypes, aspireExportAttribute)) + if (!IsAtsCompatibleValueType(paramType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes)) { reasons.Add($"parameter '{param.Name}' of type '{paramType.ToDisplayString()}' is not ATS-compatible"); } } // Check return type - if (!IsAtsCompatibleType(method.ReturnType, wellKnownTypes, aspireExportAttribute)) + if (!IsAtsCompatibleType(method.ReturnType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes)) { reasons.Add($"return type '{method.ReturnType.ToDisplayString()}' is not ATS-compatible"); } @@ -643,7 +665,8 @@ private static bool RequiresExplicitExportCoverage( IParameterSymbol param, ITypeSymbol delegateType, WellKnownTypes wellKnownTypes, - INamedTypeSymbol _) + INamedTypeSymbol aspireExportAttribute, + HashSet currentAssemblyExportedTypes) { if (delegateType is not INamedTypeSymbol namedDelegate) { @@ -661,13 +684,7 @@ private static bool RequiresExplicitExportCoverage( foreach (var delegateParam in invokeMethod.Parameters) { var dpType = delegateParam.Type; - var dpTypeName = dpType.ToDisplayString(); - - // Check for known incompatible context types - if (dpTypeName is "System.IServiceProvider" or - "System.Text.Json.Utf8JsonWriter" or - "System.Threading.CancellationToken" or - "System.Net.Http.HttpRequestMessage") + if (!IsAtsCompatibleValueType(dpType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes)) { return $"parameter '{param.Name}' uses delegate with '{dpType.Name}' which is not ATS-compatible"; } @@ -700,7 +717,8 @@ private static void AnalyzeUnionAttribute( ImmutableArray attributes, INamedTypeSymbol aspireUnionAttribute, WellKnownTypes wellKnownTypes, - INamedTypeSymbol aspireExportAttribute) + INamedTypeSymbol aspireExportAttribute, + HashSet currentAssemblyExportedTypes) { foreach (var attr in attributes) { @@ -745,7 +763,7 @@ private static void AnalyzeUnionAttribute( { if (typeConstant.Value is INamedTypeSymbol typeSymbol) { - if (!IsAtsCompatibleValueType(typeSymbol, wellKnownTypes, aspireExportAttribute)) + if (!IsAtsCompatibleValueType(typeSymbol, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes)) { context.ReportDiagnostic(Diagnostic.Create( Diagnostics.s_unionTypeMustBeAtsCompatible, @@ -853,7 +871,8 @@ private static bool IsRunSyncOnBackgroundThreadEnabled(AttributeData? exportAttr private static bool IsAtsCompatibleType( ITypeSymbol type, WellKnownTypes wellKnownTypes, - INamedTypeSymbol aspireExportAttribute) + INamedTypeSymbol aspireExportAttribute, + HashSet currentAssemblyExportedTypes) { // void is allowed if (type.SpecialType == SpecialType.System_Void) @@ -861,21 +880,22 @@ private static bool IsAtsCompatibleType( return true; } - // Task and Task are allowed (for async methods) - if (IsTaskType(type, wellKnownTypes, aspireExportAttribute)) + // Task, Task, ValueTask, and ValueTask are allowed (for async methods) + if (IsAsyncResultType(type, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes)) { return true; } - return IsAtsCompatibleValueType(type, wellKnownTypes, aspireExportAttribute); + return IsAtsCompatibleValueType(type, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); } - private static bool IsTaskType( + private static bool IsAsyncResultType( ITypeSymbol type, WellKnownTypes wellKnownTypes, - INamedTypeSymbol aspireExportAttribute) + INamedTypeSymbol aspireExportAttribute, + HashSet currentAssemblyExportedTypes) { - // Check for Task + // Check for Task / ValueTask try { var taskType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Threading_Tasks_Task); @@ -889,7 +909,20 @@ private static bool IsTaskType( // Type not found } - // Check for Task + try + { + var valueTaskType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Threading_Tasks_ValueTask); + if (SymbolEqualityComparer.Default.Equals(type, valueTaskType)) + { + return true; + } + } + catch (InvalidOperationException) + { + // Type not found + } + + // Check for Task / ValueTask if (type is INamedTypeSymbol namedType && namedType.IsGenericType) { try @@ -899,7 +932,21 @@ private static bool IsTaskType( { // Validate the T in Task is also ATS-compatible return namedType.TypeArguments.Length == 1 && - IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute); + IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); + } + } + catch (InvalidOperationException) + { + // Type not found + } + + try + { + var valueTaskOfTType = wellKnownTypes.Get(WellKnownTypeData.WellKnownType.System_Threading_Tasks_ValueTask_1); + if (SymbolEqualityComparer.Default.Equals(namedType.OriginalDefinition, valueTaskOfTType)) + { + return namedType.TypeArguments.Length == 1 && + IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); } } catch (InvalidOperationException) @@ -914,7 +961,8 @@ private static bool IsTaskType( private static bool IsAtsCompatibleValueType( ITypeSymbol type, WellKnownTypes wellKnownTypes, - INamedTypeSymbol? aspireExportAttribute = null) + INamedTypeSymbol? aspireExportAttribute = null, + HashSet? currentAssemblyExportedTypes = null) { // Handle nullable types if (type is INamedTypeSymbol namedType && @@ -939,11 +987,11 @@ private static bool IsAtsCompatibleValueType( // Arrays of ATS-compatible types if (type is IArrayTypeSymbol arrayType) { - return IsAtsCompatibleValueType(arrayType.ElementType, wellKnownTypes, aspireExportAttribute); + return IsAtsCompatibleValueType(arrayType.ElementType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); } // Collection types (Dictionary, List, IReadOnlyList, etc.) - if (IsAtsCompatibleCollectionType(type, wellKnownTypes, aspireExportAttribute)) + if (IsAtsCompatibleCollectionType(type, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes)) { return true; } @@ -961,7 +1009,7 @@ private static bool IsAtsCompatibleValueType( } // Types with [AspireExport] or [AspireDto] attribute - if (aspireExportAttribute != null && HasAspireExportAttribute(type, aspireExportAttribute)) + if (aspireExportAttribute != null && HasAspireExportAttribute(type, aspireExportAttribute, currentAssemblyExportedTypes)) { return true; } @@ -1063,7 +1111,8 @@ private static bool TryMatchGenericType(ITypeSymbol type, WellKnownTypes wellKno private static bool IsAtsCompatibleCollectionType( ITypeSymbol type, WellKnownTypes wellKnownTypes, - INamedTypeSymbol? aspireExportAttribute) + INamedTypeSymbol? aspireExportAttribute, + HashSet? currentAssemblyExportedTypes) { if (type is not INamedTypeSymbol namedType || !namedType.IsGenericType) { @@ -1076,8 +1125,8 @@ private static bool IsAtsCompatibleCollectionType( { // Validate key and value types are ATS-compatible return namedType.TypeArguments.Length == 2 && - IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute) && - IsAtsCompatibleValueType(namedType.TypeArguments[1], wellKnownTypes, aspireExportAttribute); + IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes) && + IsAtsCompatibleValueType(namedType.TypeArguments[1], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); } // List and IList @@ -1085,7 +1134,7 @@ private static bool IsAtsCompatibleCollectionType( TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IList_1)) { return namedType.TypeArguments.Length == 1 && - IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute); + IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); } // IReadOnlyList and IReadOnlyCollection @@ -1094,21 +1143,21 @@ private static bool IsAtsCompatibleCollectionType( TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IEnumerable_1)) { return namedType.TypeArguments.Length == 1 && - IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute); + IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); } // IReadOnlyDictionary if (TryMatchGenericType(type, wellKnownTypes, WellKnownTypeData.WellKnownType.System_Collections_Generic_IReadOnlyDictionary_2)) { return namedType.TypeArguments.Length == 2 && - IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute) && - IsAtsCompatibleValueType(namedType.TypeArguments[1], wellKnownTypes, aspireExportAttribute); + IsAtsCompatibleValueType(namedType.TypeArguments[0], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes) && + IsAtsCompatibleValueType(namedType.TypeArguments[1], wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); } return false; } - private static bool HasAspireExportAttribute(ITypeSymbol type, INamedTypeSymbol aspireExportAttribute) + private static bool HasAspireExportAttribute(ITypeSymbol type, INamedTypeSymbol aspireExportAttribute, HashSet? currentAssemblyExportedTypes) { // Check direct attributes on the type foreach (var attr in type.GetAttributes()) @@ -1119,6 +1168,11 @@ private static bool HasAspireExportAttribute(ITypeSymbol type, INamedTypeSymbol } } + if (currentAssemblyExportedTypes?.Contains(type) == true) + { + return true; + } + var containingAssembly = type.ContainingAssembly; if (containingAssembly is null) { @@ -1142,6 +1196,26 @@ private static bool HasAspireExportAttribute(ITypeSymbol type, INamedTypeSymbol return false; } + private static HashSet GetAssemblyExportedTypes(IAssemblySymbol assembly, INamedTypeSymbol aspireExportAttribute) + { + var exportedTypes = new HashSet(SymbolEqualityComparer.Default); + + foreach (var attr in assembly.GetAttributes()) + { + if (!SymbolEqualityComparer.Default.Equals(attr.AttributeClass, aspireExportAttribute)) + { + continue; + } + + if (TryGetAssemblyExportedType(attr, out var exportedType) && exportedType is not null) + { + exportedTypes.Add(exportedType); + } + } + + return exportedTypes; + } + private static bool TryGetAssemblyExportedType(AttributeData attribute, out ITypeSymbol? exportedType) { exportedType = null; @@ -1234,7 +1308,8 @@ private static bool IsResourceBuilderType(ITypeSymbol type, WellKnownTypes wellK private static bool IsAtsCompatibleParameter( IParameterSymbol parameter, WellKnownTypes wellKnownTypes, - INamedTypeSymbol aspireExportAttribute) + INamedTypeSymbol aspireExportAttribute, + HashSet currentAssemblyExportedTypes) { var type = parameter.Type; @@ -1247,10 +1322,10 @@ private static bool IsAtsCompatibleParameter( // params arrays are allowed if element type is compatible if (parameter.IsParams && type is IArrayTypeSymbol arrayType) { - return IsAtsCompatibleValueType(arrayType.ElementType, wellKnownTypes, aspireExportAttribute); + return IsAtsCompatibleValueType(arrayType.ElementType, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); } - return IsAtsCompatibleValueType(type, wellKnownTypes, aspireExportAttribute); + return IsAtsCompatibleValueType(type, wellKnownTypes, aspireExportAttribute, currentAssemblyExportedTypes); } private static bool IsDelegateType(ITypeSymbol type) diff --git a/src/Aspire.Hosting.RemoteHost/Aspire.Hosting.RemoteHost.csproj b/src/Aspire.Hosting.RemoteHost/Aspire.Hosting.RemoteHost.csproj index e87b27d2ab4..e55b097a6ef 100644 --- a/src/Aspire.Hosting.RemoteHost/Aspire.Hosting.RemoteHost.csproj +++ b/src/Aspire.Hosting.RemoteHost/Aspire.Hosting.RemoteHost.csproj @@ -21,6 +21,10 @@ + + + + diff --git a/src/Aspire.Hosting.RemoteHost/Ats/AtsMarshaller.cs b/src/Aspire.Hosting.RemoteHost/Ats/AtsMarshaller.cs index 2b0f2e8bd2e..9433614a013 100644 --- a/src/Aspire.Hosting.RemoteHost/Ats/AtsMarshaller.cs +++ b/src/Aspire.Hosting.RemoteHost/Ats/AtsMarshaller.cs @@ -480,6 +480,28 @@ public static bool IsSimpleType(Type type) { var underlyingType = Nullable.GetUnderlyingType(targetType) ?? targetType; + // When target type is object (union parameter), infer from JSON value + if (underlyingType == typeof(object)) + { + if (value.TryGetValue(out var s)) + { + return s; + } + if (value.TryGetValue(out var b)) + { + return b; + } + if (value.TryGetValue(out var l)) + { + return l; + } + if (value.TryGetValue(out var d)) + { + return d; + } + return value.ToJsonString(); + } + // Handle enums - they come as string names if (underlyingType.IsEnum) { diff --git a/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs b/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs index 842446b6ea0..cc219f7e2fd 100644 --- a/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs +++ b/src/Aspire.Hosting.RemoteHost/AtsCapabilityScanner.cs @@ -2538,7 +2538,8 @@ private static bool HasExposeMethodsAttribute(Type type) /// private static bool HasExportIgnoreAttribute(PropertyInfo property) { - return AttributeDataReader.HasAspireExportIgnoreData(property); + return AttributeDataReader.HasAspireExportIgnoreData(property) || + (property.DeclaringType is not null && AttributeDataReader.HasAspireExportIgnoreData(property.DeclaringType)); } /// @@ -2546,7 +2547,8 @@ private static bool HasExportIgnoreAttribute(PropertyInfo property) /// private static bool HasExportIgnoreAttribute(MethodInfo method) { - return AttributeDataReader.HasAspireExportIgnoreData(method); + return AttributeDataReader.HasAspireExportIgnoreData(method) || + (method.DeclaringType is not null && AttributeDataReader.HasAspireExportIgnoreData(method.DeclaringType)); } /// diff --git a/src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs b/src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs index ea5d6005545..02580681da6 100644 --- a/src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs +++ b/src/Aspire.Hosting.RemoteHost/CodeGeneration/CodeGenerationService.cs @@ -12,15 +12,18 @@ namespace Aspire.Hosting.RemoteHost.CodeGeneration; /// internal sealed class CodeGenerationService { + private readonly JsonRpcAuthenticationState _authenticationState; private readonly AtsContextFactory _atsContextFactory; private readonly CodeGeneratorResolver _resolver; private readonly ILogger _logger; public CodeGenerationService( + JsonRpcAuthenticationState authenticationState, AtsContextFactory atsContextFactory, CodeGeneratorResolver resolver, ILogger logger) { + _authenticationState = authenticationState; _atsContextFactory = atsContextFactory; _resolver = resolver; _logger = logger; @@ -33,6 +36,7 @@ public CodeGenerationService( [JsonRpcMethod("getCapabilities")] public CapabilitiesResponse GetCapabilities() { + _authenticationState.ThrowIfNotAuthenticated(); _logger.LogDebug(">> getCapabilities()"); var sw = System.Diagnostics.Stopwatch.StartNew(); @@ -150,6 +154,7 @@ public CapabilitiesResponse GetCapabilities() [JsonRpcMethod("generateCode")] public Dictionary GenerateCode(string language) { + _authenticationState.ThrowIfNotAuthenticated(); _logger.LogDebug(">> generateCode({Language})", language); var sw = System.Diagnostics.Stopwatch.StartNew(); diff --git a/src/Aspire.Hosting.RemoteHost/JsonRpcAuthenticationState.cs b/src/Aspire.Hosting.RemoteHost/JsonRpcAuthenticationState.cs new file mode 100644 index 00000000000..dff2603cb75 --- /dev/null +++ b/src/Aspire.Hosting.RemoteHost/JsonRpcAuthenticationState.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Cryptography; +using System.Text; +using Microsoft.Extensions.Configuration; + +namespace Aspire.Hosting.RemoteHost; + +internal sealed class JsonRpcAuthenticationState +{ + private readonly byte[]? _expectedTokenBytes; + + public JsonRpcAuthenticationState(IConfiguration configuration) + { + if (configuration[KnownConfigNames.RemoteAppHostToken] is { Length: > 0 } token) + { + _expectedTokenBytes = Encoding.UTF8.GetBytes(token); + } + + IsAuthenticated = _expectedTokenBytes is null; + } + + public bool IsAuthenticated { get; private set; } + + public bool Authenticate(string token) + { + if (IsAuthenticated) + { + return true; + } + + if (_expectedTokenBytes is null) + { + IsAuthenticated = true; + return true; + } + + if (string.IsNullOrEmpty(token)) + { + return false; + } + + var providedTokenBytes = Encoding.UTF8.GetBytes(token); + + try + { + var isMatch = CryptographicOperations.FixedTimeEquals(providedTokenBytes, _expectedTokenBytes); + + if (isMatch) + { + IsAuthenticated = true; + } + + return isMatch; + } + finally + { + CryptographicOperations.ZeroMemory(providedTokenBytes); + } + } + + public void ThrowIfNotAuthenticated() + { + if (!IsAuthenticated) + { + throw new InvalidOperationException("Client must authenticate before invoking AppHost RPC methods."); + } + } +} diff --git a/src/Aspire.Hosting.RemoteHost/JsonRpcServer.cs b/src/Aspire.Hosting.RemoteHost/JsonRpcServer.cs index 5128be80e0e..55585cae34f 100644 --- a/src/Aspire.Hosting.RemoteHost/JsonRpcServer.cs +++ b/src/Aspire.Hosting.RemoteHost/JsonRpcServer.cs @@ -20,8 +20,6 @@ internal sealed class JsonRpcServer : BackgroundService { private readonly string _socketPath; private readonly IServiceScopeFactory _scopeFactory; - private readonly CodeGenerationService _codeGenerationService; - private readonly LanguageService _languageService; private readonly ILogger _logger; private Socket? _listenSocket; private bool _disposed; @@ -30,13 +28,9 @@ internal sealed class JsonRpcServer : BackgroundService public JsonRpcServer( IConfiguration configuration, IServiceScopeFactory scopeFactory, - CodeGenerationService codeGenerationService, - LanguageService languageService, ILogger logger) { _scopeFactory = scopeFactory; - _codeGenerationService = codeGenerationService; - _languageService = languageService; _logger = logger; var socketPath = configuration["REMOTE_APP_HOST_SOCKET_PATH"]; @@ -194,6 +188,8 @@ private async Task HandleClientStreamAsync(Stream clientStream, bool ownsStream, // Resolve the scoped RemoteAppHostService var clientService = scope.ServiceProvider.GetRequiredService(); + var codeGenerationService = scope.ServiceProvider.GetRequiredService(); + var languageService = scope.ServiceProvider.GetRequiredService(); try { @@ -203,10 +199,10 @@ private async Task HandleClientStreamAsync(Stream clientStream, bool ownsStream, using var jsonRpc = new JsonRpc(handler, clientService); // Add the shared CodeGenerationService as an additional target for generateCode method - jsonRpc.AddLocalRpcTarget(_codeGenerationService); + jsonRpc.AddLocalRpcTarget(codeGenerationService); // Add the shared LanguageService as an additional target for language support methods - jsonRpc.AddLocalRpcTarget(_languageService); + jsonRpc.AddLocalRpcTarget(languageService); jsonRpc.StartListening(); diff --git a/src/Aspire.Hosting.RemoteHost/Language/LanguageService.cs b/src/Aspire.Hosting.RemoteHost/Language/LanguageService.cs index 7a14aa97887..738611bdb57 100644 --- a/src/Aspire.Hosting.RemoteHost/Language/LanguageService.cs +++ b/src/Aspire.Hosting.RemoteHost/Language/LanguageService.cs @@ -13,13 +13,16 @@ namespace Aspire.Hosting.RemoteHost.Language; /// internal sealed class LanguageService { + private readonly JsonRpcAuthenticationState _authenticationState; private readonly LanguageSupportResolver _resolver; private readonly ILogger _logger; public LanguageService( + JsonRpcAuthenticationState authenticationState, LanguageSupportResolver resolver, ILogger logger) { + _authenticationState = authenticationState; _resolver = resolver; _logger = logger; } @@ -34,6 +37,7 @@ public LanguageService( [JsonRpcMethod("scaffoldAppHost")] public Dictionary ScaffoldAppHost(string language, string targetPath, string? projectName = null) { + _authenticationState.ThrowIfNotAuthenticated(); _logger.LogDebug(">> scaffoldAppHost({Language}, {TargetPath}, {ProjectName})", language, targetPath, projectName); var sw = Stopwatch.StartNew(); @@ -71,6 +75,7 @@ public Dictionary ScaffoldAppHost(string language, string target [JsonRpcMethod("detectAppHostType")] public DetectionResult DetectAppHostType(string directoryPath) { + _authenticationState.ThrowIfNotAuthenticated(); _logger.LogDebug(">> detectAppHostType({DirectoryPath})", directoryPath); var sw = System.Diagnostics.Stopwatch.StartNew(); @@ -104,6 +109,7 @@ public DetectionResult DetectAppHostType(string directoryPath) [JsonRpcMethod("getRuntimeSpec")] public RuntimeSpec GetRuntimeSpec(string language) { + _authenticationState.ThrowIfNotAuthenticated(); _logger.LogDebug(">> getRuntimeSpec({Language})", language); var sw = System.Diagnostics.Stopwatch.StartNew(); diff --git a/src/Aspire.Hosting.RemoteHost/RemoteAppHostService.cs b/src/Aspire.Hosting.RemoteHost/RemoteAppHostService.cs index f67cd6d0fb9..d2f9aa48764 100644 --- a/src/Aspire.Hosting.RemoteHost/RemoteAppHostService.cs +++ b/src/Aspire.Hosting.RemoteHost/RemoteAppHostService.cs @@ -10,19 +10,23 @@ namespace Aspire.Hosting.RemoteHost; internal sealed class RemoteAppHostService { + private readonly JsonRpcAuthenticationState _authenticationState; private readonly JsonRpcCallbackInvoker _callbackInvoker; private readonly CancellationTokenRegistry _cancellationTokenRegistry; private readonly ILogger _logger; + private JsonRpc? _clientRpc; // ATS (Aspire Type System) components private readonly CapabilityDispatcher _capabilityDispatcher; public RemoteAppHostService( + JsonRpcAuthenticationState authenticationState, JsonRpcCallbackInvoker callbackInvoker, CancellationTokenRegistry cancellationTokenRegistry, CapabilityDispatcher capabilityDispatcher, ILogger logger) { + _authenticationState = authenticationState; _callbackInvoker = callbackInvoker; _cancellationTokenRegistry = cancellationTokenRegistry; _capabilityDispatcher = capabilityDispatcher; @@ -34,9 +38,29 @@ public RemoteAppHostService( /// public void SetClientConnection(JsonRpc clientRpc) { + _clientRpc = clientRpc; _callbackInvoker.SetConnection(clientRpc); } + /// + /// Verifies the authentication token supplied by the client. + /// Returns true on success; closes the connection and returns false on failure + /// so that an unauthenticated client cannot keep retrying without limit. + /// + [JsonRpcMethod("authenticate")] + public bool Authenticate(string token) + { + var authenticated = _authenticationState.Authenticate(token); + if (!authenticated) + { + _logger.LogWarning("Rejected unauthenticated AppHost RPC client."); + // Close the connection to prevent unlimited retry attempts. + _ = Task.Run(() => _clientRpc?.Dispose()); + } + + return authenticated; + } + [JsonRpcMethod("ping")] #pragma warning disable CA1822 // Mark members as static - JSON-RPC methods must be instance methods public string Ping() @@ -54,6 +78,7 @@ public string Ping() [JsonRpcMethod("cancelToken")] public bool CancelToken(string tokenId) { + _authenticationState.ThrowIfNotAuthenticated(); _logger.LogDebug("cancelToken({TokenId})", tokenId); return _cancellationTokenRegistry.Cancel(tokenId); } @@ -69,6 +94,7 @@ public bool CancelToken(string tokenId) [JsonRpcMethod("invokeCapability")] public async Task InvokeCapabilityAsync(string capabilityId, JsonObject? args) { + _authenticationState.ThrowIfNotAuthenticated(); _logger.LogDebug(">> invokeCapability({CapabilityId}) args: {Args}", capabilityId, args?.ToJsonString() ?? "null"); var sw = System.Diagnostics.Stopwatch.StartNew(); try diff --git a/src/Aspire.Hosting.RemoteHost/RemoteHostServer.cs b/src/Aspire.Hosting.RemoteHost/RemoteHostServer.cs index 61aacab0f6d..5de5abe83ce 100644 --- a/src/Aspire.Hosting.RemoteHost/RemoteHostServer.cs +++ b/src/Aspire.Hosting.RemoteHost/RemoteHostServer.cs @@ -46,11 +46,12 @@ private static void ConfigureServices(IServiceCollection services) services.AddSingleton(); services.AddSingleton(sp => sp.GetRequiredService().GetContext()); services.AddSingleton(); - services.AddSingleton(); services.AddSingleton(); - services.AddSingleton(); - // Register scoped services for per-client state + // Scoped services + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Aspire.Hosting/ApplicationModel/DistributedApplicationModelExtensions.cs b/src/Aspire.Hosting/ApplicationModel/DistributedApplicationModelExtensions.cs index e72f7464d3d..f355627bc81 100644 --- a/src/Aspire.Hosting/ApplicationModel/DistributedApplicationModelExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/DistributedApplicationModelExtensions.cs @@ -14,6 +14,7 @@ public static class DistributedApplicationModelExtensions /// /// The distributed application model to extract compute resources from. /// An enumerable of compute in the model. + [AspireExportIgnore(Reason = "Application model inspection helper — not part of the ATS surface.")] public static IEnumerable GetComputeResources(this DistributedApplicationModel model) { foreach (var r in model.Resources) @@ -43,6 +44,7 @@ public static IEnumerable GetComputeResources(this DistributedApplica /// /// The distributed application model to extract build resources from. /// An enumerable of build in the model. + [AspireExportIgnore(Reason = "Application model inspection helper — not part of the ATS surface.")] public static IEnumerable GetBuildResources(this DistributedApplicationModel model) { foreach (var r in model.Resources) @@ -60,6 +62,7 @@ public static IEnumerable GetBuildResources(this DistributedApplicati /// /// The distributed application model to extract build and push resources from. /// An enumerable of build and push in the model. + [AspireExportIgnore(Reason = "Application model inspection helper — not part of the ATS surface.")] public static IEnumerable GetBuildAndPushResources(this DistributedApplicationModel model) { foreach (var r in model.Resources) diff --git a/src/Aspire.Hosting/ApplicationModel/ProjectResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ProjectResourceExtensions.cs index 8ce815484eb..27b79fabf6e 100644 --- a/src/Aspire.Hosting/ApplicationModel/ProjectResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ProjectResourceExtensions.cs @@ -13,6 +13,7 @@ public static class ProjectResourceExtensions /// /// The distributed application model. /// An enumerable collection of project resources. + [AspireExportIgnore(Reason = "Application model inspection helper — not part of the ATS surface.")] public static IEnumerable GetProjectResources(this DistributedApplicationModel model) { ArgumentNullException.ThrowIfNull(model); @@ -26,6 +27,7 @@ public static IEnumerable GetProjectResources(this DistributedA /// The project resource. /// The project metadata. /// Thrown when the project resource doesn't have project metadata. + [AspireExportIgnore(Reason = "Project metadata is a .NET-specific contract and is not part of the ATS surface.")] public static IProjectMetadata GetProjectMetadata(this ProjectResource projectResource) { ArgumentNullException.ThrowIfNull(projectResource); diff --git a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs index 1296b4d104e..644ebfe0df8 100644 --- a/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs +++ b/src/Aspire.Hosting/ApplicationModel/ResourceExtensions.cs @@ -24,6 +24,7 @@ public static class ResourceExtensions /// The resource to get the annotation from. /// When this method returns, contains the last annotation of the specified type from the resource, if found; otherwise, the default value for . /// if the last annotation of the specified type was found in the resource; otherwise, . + [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")] public static bool TryGetLastAnnotation(this IResource resource, [NotNullWhen(true)] out T? annotation) where T : IResourceAnnotation { if (resource.Annotations.OfType().LastOrDefault() is { } lastAnnotation) @@ -45,6 +46,7 @@ public static bool TryGetLastAnnotation(this IResource resource, [NotNullWhen /// The resource to retrieve annotations from. /// When this method returns, contains the annotations of the specified type, if found; otherwise, . /// if annotations of the specified type were found; otherwise, . + [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")] public static bool TryGetAnnotationsOfType(this IResource resource, [NotNullWhen(true)] out IEnumerable? result) where T : IResourceAnnotation { var matchingTypeAnnotations = resource.Annotations.OfType(); @@ -67,6 +69,7 @@ public static bool TryGetAnnotationsOfType(this IResource resource, [NotNullW /// The type of annotation to retrieve. /// The resource to retrieve annotations from. /// if an annotation of the specified type was found; otherwise, . + [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")] public static bool HasAnnotationOfType(this IResource resource) where T : IResourceAnnotation { return resource.Annotations.Any(a => a is T); @@ -79,6 +82,7 @@ public static bool HasAnnotationOfType(this IResource resource) where T : IRe /// The resource to retrieve annotations from. /// When this method returns, contains the annotations of the specified type, if found; otherwise, . /// if annotations of the specified type were found; otherwise, . + [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")] public static bool TryGetAnnotationsIncludingAncestorsOfType(this IResource resource, [NotNullWhen(true)] out IEnumerable? result) where T : IResourceAnnotation { if (resource is IResourceWithParent) @@ -116,6 +120,7 @@ public static bool TryGetAnnotationsIncludingAncestorsOfType(this IResource r /// The type of annotation to retrieve. /// The resource to retrieve annotations from. /// if an annotation of the specified type was found; otherwise, . + [AspireExportIgnore(Reason = "Generic annotation inspection helper — not part of the ATS surface.")] public static bool HasAnnotationIncludingAncestorsOfType(this IResource resource) where T : IResourceAnnotation { if (resource is IResourceWithParent) @@ -149,6 +154,7 @@ public static bool HasAnnotationIncludingAncestorsOfType(this IResource resou /// The resource to get the environment variables from. /// The environment variables retrieved from the resource, if any. /// True if the environment variables were successfully retrieved, false otherwise. + [AspireExportIgnore(Reason = "Environment callback inspection helper — not part of the ATS surface.")] public static bool TryGetEnvironmentVariables(this IResource resource, [NotNullWhen(true)] out IEnumerable? environmentVariables) { return TryGetAnnotationsOfType(resource, out environmentVariables); @@ -545,6 +551,7 @@ internal static IEnumerable GetSupportedNetworks(this IResour /// Gets a value indicating whether the resource is excluded from being published. /// /// The resource to determine if it should be excluded from being published. + [AspireExportIgnore(Reason = "Manifest inspection helper — not part of the ATS surface.")] public static bool IsExcludedFromPublish(this IResource resource) => resource.TryGetLastAnnotation(out var lastAnnotation) && lastAnnotation == ManifestPublishingCallbackAnnotation.Ignore; @@ -655,6 +662,7 @@ private static bool TryGetEndpointReference(IValueProvider valueProvider, [NotNu /// The resource to get the volume mounts for. /// When this method returns, contains the volume mounts for the specified resource, if found; otherwise, null. /// true if the volume mounts were successfully retrieved; otherwise, false. + [AspireExportIgnore(Reason = "Container mount inspection helper — not part of the ATS surface.")] public static bool TryGetContainerMounts(this IResource resource, [NotNullWhen(true)] out IEnumerable? volumeMounts) { return TryGetAnnotationsOfType(resource, out volumeMounts); @@ -666,6 +674,7 @@ public static bool TryGetContainerMounts(this IResource resource, [NotNullWhen(t /// The resource to retrieve the endpoints for. /// The endpoints for the given resource, if found. /// True if the endpoints were found, false otherwise. + [AspireExportIgnore(Reason = "Endpoint annotation inspection helper — not part of the ATS surface.")] public static bool TryGetEndpoints(this IResource resource, [NotNullWhen(true)] out IEnumerable? endpoints) { return TryGetAnnotationsOfType(resource, out endpoints); @@ -677,6 +686,7 @@ public static bool TryGetEndpoints(this IResource resource, [NotNullWhen(true)] /// The resource to retrieve the URLs for. /// The URLs for the given resource, if found. /// True if the URLs were found, false otherwise. + [AspireExportIgnore(Reason = "URL annotation inspection helper — not part of the ATS surface.")] public static bool TryGetUrls(this IResource resource, [NotNullWhen(true)] out IEnumerable? urls) { return TryGetAnnotationsOfType(resource, out urls); @@ -687,6 +697,7 @@ public static bool TryGetUrls(this IResource resource, [NotNullWhen(true)] out I /// /// The which contains annotations. /// An enumeration of based on the annotations from the resources' collection. + [AspireExportIgnore(Reason = "Resource handle endpoint enumeration is not part of the ATS surface; use builder-based endpoint exports instead.")] public static IEnumerable GetEndpoints(this IResourceWithEndpoints resource) { if (TryGetAnnotationsOfType(resource, out var endpoints)) @@ -703,6 +714,7 @@ public static IEnumerable GetEndpoints(this IResourceWithEndp /// The which contains annotations. /// The ID of the network that serves as the context context for the endpoint references. /// An enumeration of based on the annotations from the resources' collection. + [AspireExportIgnore(Reason = "Network-specific endpoint enumeration is not part of the ATS surface.")] public static IEnumerable GetEndpoints(this IResourceWithEndpoints resource, NetworkIdentifier contextNetworkID) { if (TryGetAnnotationsOfType(resource, out var endpoints)) @@ -719,6 +731,7 @@ public static IEnumerable GetEndpoints(this IResourceWithEndp /// The which contains annotations. /// The name of the endpoint. /// An object providing resolvable reference for the specified endpoint. + [AspireExportIgnore(Reason = "Resource handle endpoint lookup is not part of the ATS surface; use builder-based endpoint exports instead.")] public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName) { var endpoint = resource.TryGetEndpoints(out var endpoints) ? @@ -741,6 +754,7 @@ public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource /// The name of the endpoint. /// The network ID of the network that provides the context for the returned /// An object providing resolvable reference for the specified endpoint. + [AspireExportIgnore(Reason = "Network-specific endpoint lookup is not part of the ATS surface.")] public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource, string endpointName, NetworkIdentifier contextNetworkID) { @@ -765,6 +779,7 @@ public static EndpointReference GetEndpoint(this IResourceWithEndpoints resource /// The resource containing endpoints to resolve. /// Optional port allocator. If null, uses default allocation starting from port 8000. /// A read-only list of resolved endpoints with computed port values. + [AspireExportIgnore(Reason = "Endpoint resolution exposes infrastructure-specific types that are not part of the ATS surface.")] public static IReadOnlyList ResolveEndpoints(this IResource resource, IPortAllocator? portAllocator = null) { if (!resource.TryGetEndpoints(out var endpoints)) @@ -848,6 +863,7 @@ public static IReadOnlyList ResolveEndpoints(this IResource re /// The resource to get the container image name from. /// The container image name if found, otherwise null. /// True if the container image name was found, otherwise false. + [AspireExportIgnore(Reason = "Container image inspection helper — not part of the ATS surface.")] public static bool TryGetContainerImageName(this IResource resource, [NotNullWhen(true)] out string? imageName) { return TryGetContainerImageName(resource, useBuiltImage: true, out imageName); @@ -860,6 +876,7 @@ public static bool TryGetContainerImageName(this IResource resource, [NotNullWhe /// When true, uses the image name from DockerfileBuildAnnotation if present. When false, uses only ContainerImageAnnotation. /// The container image name if found, otherwise null. /// True if the container image name was found, otherwise false. + [AspireExportIgnore(Reason = "Container image inspection helper — not part of the ATS surface.")] public static bool TryGetContainerImageName(this IResource resource, bool useBuiltImage, [NotNullWhen(true)] out string? imageName) { // First check if there's a DockerfileBuildAnnotation with an image name/tag @@ -901,6 +918,7 @@ public static bool TryGetContainerImageName(this IResource resource, bool useBui /// /// The resource to get the replica count for. /// The number of replicas for the specified resource. + [AspireExportIgnore(Reason = "Replica inspection helper — not part of the ATS surface.")] public static int GetReplicaCount(this IResource resource) { if (resource.TryGetLastAnnotation(out var replicaAnnotation)) @@ -922,6 +940,7 @@ public static int GetReplicaCount(this IResource resource) /// /// The resource to evaluate for image build requirements. /// True if the resource requires image building; otherwise, false. + [AspireExportIgnore(Reason = "Publishing inspection helper — not part of the ATS surface.")] public static bool RequiresImageBuild(this IResource resource) { if (resource.IsExcludedFromPublish()) @@ -942,6 +961,7 @@ public static bool RequiresImageBuild(this IResource resource) /// /// The resource to evaluate for image push requirements. /// True if the resource requires image building and pushing; otherwise, false. + [AspireExportIgnore(Reason = "Publishing inspection helper — not part of the ATS surface.")] public static bool RequiresImageBuildAndPush(this IResource resource) { return resource.RequiresImageBuild() && !resource.IsBuildOnlyContainer(); @@ -958,6 +978,7 @@ internal static bool IsBuildOnlyContainer(this IResource resource) /// /// The resource to get the compute environment for. /// The compute environment the resource is bound to, or null if the resource is not bound to any specific compute environment. + [AspireExportIgnore(Reason = "Compute-environment inspection helper — not part of the ATS surface.")] public static IComputeEnvironmentResource? GetComputeEnvironment(this IResource resource) { if (resource.TryGetLastAnnotation(out var computeEnvironmentAnnotation)) @@ -971,6 +992,7 @@ internal static bool IsBuildOnlyContainer(this IResource resource) /// Gets the deployment target for the specified resource, if any. Throws an exception if /// there are multiple compute environments and a compute environment is not explicitly specified. /// + [AspireExportIgnore(Reason = "Deployment target inspection helper — not part of the ATS surface.")] public static DeploymentTargetAnnotation? GetDeploymentTargetAnnotation(this IResource resource, IComputeEnvironmentResource? targetComputeEnvironment = null) { IComputeEnvironmentResource? selectedComputeEnvironment = null; @@ -1278,6 +1300,7 @@ internal static ILogger GetLogger(this IResource resource, IServiceProvider serv /// This method invokes environment variable and command-line argument callbacks to discover all references. The context resource () is not considered a dependency (even if it is transitively referenced). /// /// + [AspireExportIgnore(Reason = "Dependency discovery helper depends on execution context and is not part of the ATS surface.")] public static Task> GetResourceDependenciesAsync( this IResource resource, DistributedApplicationExecutionContext executionContext, diff --git a/src/Aspire.Hosting/Aspire.Hosting.csproj b/src/Aspire.Hosting/Aspire.Hosting.csproj index e8023ada94a..a734999211c 100644 --- a/src/Aspire.Hosting/Aspire.Hosting.csproj +++ b/src/Aspire.Hosting/Aspire.Hosting.csproj @@ -132,11 +132,6 @@ - - - - - $(BeforePack);IncludeIntegrationAnalyzerInPackage diff --git a/src/Aspire.Hosting/Ats/AspireExportIgnoreAttribute.cs b/src/Aspire.Hosting/Ats/AspireExportIgnoreAttribute.cs index fc6c4662bb6..67927023ea6 100644 --- a/src/Aspire.Hosting/Ats/AspireExportIgnoreAttribute.cs +++ b/src/Aspire.Hosting/Ats/AspireExportIgnoreAttribute.cs @@ -6,7 +6,7 @@ namespace Aspire.Hosting; /// -/// Excludes a property or method from ATS export when the containing type uses +/// Excludes a property, method, or type from ATS export when the containing type uses /// or . /// /// @@ -18,6 +18,10 @@ namespace Aspire.Hosting; /// This is useful when most members should be exposed but a few contain internal /// implementation details or types that shouldn't be part of the polyglot API. /// +/// +/// Apply this attribute to a type to suppress all automatic export coverage checks for the +/// type's members when the type is intentionally not part of the ATS surface. +/// /// /// /// @@ -33,7 +37,7 @@ namespace Aspire.Hosting; /// /// [AttributeUsage( - AttributeTargets.Property | AttributeTargets.Method, + AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Method, Inherited = false, AllowMultiple = false)] [Experimental("ASPIREATS001")] diff --git a/src/Aspire.Hosting/ConnectionPropertiesExtensions.cs b/src/Aspire.Hosting/ConnectionPropertiesExtensions.cs index ba0659d568d..fe3e7193891 100644 --- a/src/Aspire.Hosting/ConnectionPropertiesExtensions.cs +++ b/src/Aspire.Hosting/ConnectionPropertiesExtensions.cs @@ -16,6 +16,7 @@ public static class ConnectionPropertiesExtensions /// The resource that exposes the base connection properties. /// The additional connection properties to merge into the values supplied by . /// A sequence that contains the combined set of connection properties with duplicate keys resolved in favor of . + [AspireExportIgnore(Reason = "Connection property merging is an internal helper and is not part of the ATS surface.")] public static IEnumerable> CombineProperties(this IResourceWithConnectionString source, IEnumerable> additional) { ArgumentNullException.ThrowIfNull(source); @@ -35,4 +36,4 @@ public static IEnumerable> CombineProp return dict.AsEnumerable(); } -} \ No newline at end of file +} diff --git a/src/Aspire.Hosting/ConnectionStringBuilderExtensions.cs b/src/Aspire.Hosting/ConnectionStringBuilderExtensions.cs index 05975ad903f..2e53ac75db5 100644 --- a/src/Aspire.Hosting/ConnectionStringBuilderExtensions.cs +++ b/src/Aspire.Hosting/ConnectionStringBuilderExtensions.cs @@ -142,7 +142,7 @@ await evt.Notifications.PublishUpdateAsync(r, s => s with /// builder.Build().Run(); /// /// - [AspireExport("addConnectionStringBuilder", Description = "Adds a connection string with a builder callback")] + [AspireExport("addConnectionStringBuilder", Description = "Adds a connection string with a builder callback", RunSyncOnBackgroundThread = true)] public static IResourceBuilder AddConnectionString(this IDistributedApplicationBuilder builder, [ResourceName] string name, Action connectionStringBuilder) { var rb = new ReferenceExpressionBuilder(); diff --git a/src/Aspire.Hosting/ContainerExecutableResourceExtensions.cs b/src/Aspire.Hosting/ContainerExecutableResourceExtensions.cs index c58cdf75d9f..b182c0faeae 100644 --- a/src/Aspire.Hosting/ContainerExecutableResourceExtensions.cs +++ b/src/Aspire.Hosting/ContainerExecutableResourceExtensions.cs @@ -15,6 +15,7 @@ internal static class ContainerExecutableResourceExtensions /// /// The distributed application model to retrieve executable resources from. /// An enumerable collection of executable resources. + [AspireExportIgnore(Reason = "Application model inspection helper — not part of the ATS surface.")] public static IEnumerable GetContainerExecutableResources(this DistributedApplicationModel model) { ArgumentNullException.ThrowIfNull(model); diff --git a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs index 54b059c81c1..d97965c4419 100644 --- a/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceBuilderExtensions.cs @@ -1091,7 +1091,7 @@ public static IResourceBuilder WithBuildArg(this IResourceBuilder build /// /// /// - [AspireExport("withBuildArg", Description = "Adds a build argument from a parameter resource")] + [AspireExport("withParameterBuildArg", MethodName = "withBuildArg", Description = "Adds a build argument from a parameter resource")] public static IResourceBuilder WithBuildArg(this IResourceBuilder builder, string name, IResourceBuilder value) where T : ContainerResource { ArgumentNullException.ThrowIfNull(builder); @@ -1139,7 +1139,7 @@ public static IResourceBuilder WithBuildArg(this IResourceBuilder build /// /// /// - [AspireExport("withBuildSecret", Description = "Adds a build secret from a parameter resource")] + [AspireExport("withParameterBuildSecret", MethodName = "withBuildSecret", Description = "Adds a build secret from a parameter resource")] public static IResourceBuilder WithBuildSecret(this IResourceBuilder builder, string name, IResourceBuilder value) where T : ContainerResource { ArgumentNullException.ThrowIfNull(builder); diff --git a/src/Aspire.Hosting/ContainerResourceExtensions.cs b/src/Aspire.Hosting/ContainerResourceExtensions.cs index ea48e0b5575..79ea9e54bfc 100644 --- a/src/Aspire.Hosting/ContainerResourceExtensions.cs +++ b/src/Aspire.Hosting/ContainerResourceExtensions.cs @@ -15,6 +15,7 @@ public static class ContainerResourceExtensions /// /// The distributed application model to search for container resources. /// A collection of container resources in the specified distributed application model. + [AspireExportIgnore(Reason = "Application model inspection helper — not part of the ATS surface.")] public static IEnumerable GetContainerResources(this DistributedApplicationModel model) { ArgumentNullException.ThrowIfNull(model); @@ -33,6 +34,7 @@ public static IEnumerable GetContainerResources(this DistributedAppli /// /// The resource to check. /// true if the specified resource is a container resource; otherwise, false. + [AspireExportIgnore(Reason = "Application model inspection helper — not part of the ATS surface.")] public static bool IsContainer(this IResource resource) { ArgumentNullException.ThrowIfNull(resource); diff --git a/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto b/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto index a7f7fe37998..2cd238327f4 100644 --- a/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto +++ b/src/Aspire.Hosting/Dashboard/proto/dashboard_service.proto @@ -186,12 +186,12 @@ message ResourceRelationship { message ResourceProperty { // Name of the data item, e.g. "container.id", "executable.pid", "project.path", ... string name = 1; - // TODO move display_name to reference data, sent once when the connection starts (https://github.com/dotnet/aspire/issues/1644) + // TODO move display_name to reference data, sent once when the connection starts (https://github.com/microsoft/aspire/issues/1644) // Optional display name, may be localized optional string display_name = 2; // The data value. May be null, a number, a string, a boolean, a dictionary of values (Struct), or a list of values (ValueList). google.protobuf.Value value = 3; - // TODO move is_sensitive to reference data, sent once when the connection starts (https://github.com/dotnet/aspire/issues/1644) + // TODO move is_sensitive to reference data, sent once when the connection starts (https://github.com/microsoft/aspire/issues/1644) // Whether the value is sensitive and should be masked in the UI by default. // Defaults to false. When true, the user must explicitly unmask the value to view it. optional bool is_sensitive = 4; diff --git a/src/Aspire.Hosting/EmulatorResourceExtensions.cs b/src/Aspire.Hosting/EmulatorResourceExtensions.cs index b6199786714..28f04454833 100644 --- a/src/Aspire.Hosting/EmulatorResourceExtensions.cs +++ b/src/Aspire.Hosting/EmulatorResourceExtensions.cs @@ -15,6 +15,7 @@ public static class EmulatorResourceExtensions /// /// The resource to check. /// true if the specified resource is an emulator resource; otherwise, false. + [AspireExportIgnore(Reason = "Application model inspection helper — not part of the ATS surface.")] public static bool IsEmulator(this IResource resource) { ArgumentNullException.ThrowIfNull(resource); diff --git a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs index 53c28d1174b..8a87eda8c17 100644 --- a/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ExecutableResourceBuilderExtensions.cs @@ -117,7 +117,7 @@ public static IResourceBuilder PublishAsDockerFile(this IResourceBuilderResource builder /// Optional action to configure the container resource /// A reference to the . - [AspireExport("publishAsDockerFileWithConfigure", Description = "Publishes an executable as a Docker file with optional container configuration")] + [AspireExport("publishAsDockerFileWithConfigure", Description = "Publishes an executable as a Docker file with optional container configuration", RunSyncOnBackgroundThread = true)] public static IResourceBuilder PublishAsDockerFile(this IResourceBuilder builder, Action>? configure) where T : ExecutableResource { diff --git a/src/Aspire.Hosting/ExecutableResourceExtensions.cs b/src/Aspire.Hosting/ExecutableResourceExtensions.cs index 69fccd1f39a..3f9766081a8 100644 --- a/src/Aspire.Hosting/ExecutableResourceExtensions.cs +++ b/src/Aspire.Hosting/ExecutableResourceExtensions.cs @@ -15,6 +15,7 @@ public static class ExecutableResourceExtensions /// /// The distributed application model to retrieve executable resources from. /// An enumerable collection of executable resources. + [AspireExportIgnore(Reason = "Application model inspection helper — not part of the ATS surface.")] public static IEnumerable GetExecutableResources(this DistributedApplicationModel model) { ArgumentNullException.ThrowIfNull(model); diff --git a/src/Aspire.Hosting/Polyglot/PolyglotIgnoreAttribute.cs b/src/Aspire.Hosting/Polyglot/PolyglotIgnoreAttribute.cs deleted file mode 100644 index 9395285b9ae..00000000000 --- a/src/Aspire.Hosting/Polyglot/PolyglotIgnoreAttribute.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Aspire.Hosting.Polyglot; - -/// -/// Marks a method or type as excluded from polyglot code generation. -/// -/// -/// Use this attribute to prevent specific methods, types, or properties from being included -/// in generated TypeScript/Python SDK bindings. This is useful for methods that: -/// -/// Have unsupported parameter types or signatures -/// Are intended only for internal use -/// Would create confusing or unnecessary API surface in guest languages -/// -/// -/// -/// -/// // This method will not appear in generated SDKs -/// [PolyglotIgnore] -/// public static IResourceBuilder<T> WithAdvancedConfiguration<T>( -/// this IResourceBuilder<T> builder, -/// Action<IConfiguration, IServiceProvider> configure); -/// -/// -[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] -public sealed class PolyglotIgnoreAttribute : Attribute -{ - /// - /// Initializes a new instance of the class. - /// - public PolyglotIgnoreAttribute() - { - Languages = PolyglotLanguages.All; - } - - /// - /// Initializes a new instance of the class - /// for specific languages only. - /// - /// The languages for which this member should be ignored. - public PolyglotIgnoreAttribute(PolyglotLanguages languages) - { - Languages = languages; - } - - /// - /// Gets the languages for which this member should be ignored. - /// - public PolyglotLanguages Languages { get; } -} diff --git a/src/Aspire.Hosting/Polyglot/PolyglotLanguages.cs b/src/Aspire.Hosting/Polyglot/PolyglotLanguages.cs deleted file mode 100644 index 47211946152..00000000000 --- a/src/Aspire.Hosting/Polyglot/PolyglotLanguages.cs +++ /dev/null @@ -1,35 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Aspire.Hosting.Polyglot; - -/// -/// Specifies the set of supported programming languages for polyglot operations. -/// -/// -/// This enumeration supports bitwise combination of its member values. Use the Flags attribute to -/// represent multiple languages simultaneously. -/// -[Flags] -public enum PolyglotLanguages -{ - /// - /// Indicates that no languages are specified. - /// - None = 0, - - /// - /// Indicates TypeScript/JavaScript language support. - /// - TypeScript = 1 << 0, - - /// - /// Indicates Python language support. - /// - Python = 1 << 1, - - /// - /// Indicates all supported languages. - /// - All = TypeScript | Python -} diff --git a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs index 8ce59028c23..3b5577ad18a 100644 --- a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs @@ -282,7 +282,7 @@ public static IResourceBuilder AddProject(this IDistributedAppl /// /// /// - [AspireExport("addProjectWithOptions", Description = "Adds a project resource with configuration options")] + [AspireExport("addProjectWithOptions", Description = "Adds a project resource with configuration options", RunSyncOnBackgroundThread = true)] public static IResourceBuilder AddProject(this IDistributedApplicationBuilder builder, [ResourceName] string name, string projectPath, Action configure) { ArgumentNullException.ThrowIfNull(builder); @@ -364,7 +364,7 @@ public static IResourceBuilder AddCSharpApp(this IDistributedAp /// /// [Experimental("ASPIRECSHARPAPPS001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport("addCSharpAppWithOptions", Description = "Adds a C# application resource with configuration options")] + [AspireExport("addCSharpAppWithOptions", Description = "Adds a C# application resource with configuration options", RunSyncOnBackgroundThread = true)] public static IResourceBuilder AddCSharpApp(this IDistributedApplicationBuilder builder, [ResourceName] string name, string path, Action configure) { ArgumentNullException.ThrowIfNull(builder); @@ -826,7 +826,7 @@ public static IResourceBuilder WithEndpointsInEnvironment( /// Resource builder /// Optional action to configure the container resource /// A reference to the . - [AspireExport("publishProjectAsDockerFileWithConfigure", MethodName = "publishAsDockerFile", Description = "Publishes a project as a Docker file with optional container configuration")] + [AspireExport("publishProjectAsDockerFileWithConfigure", MethodName = "publishAsDockerFile", Description = "Publishes a project as a Docker file with optional container configuration", RunSyncOnBackgroundThread = true)] public static IResourceBuilder PublishAsDockerFile(this IResourceBuilder builder, Action>? configure = null) where T : ProjectResource { diff --git a/src/Aspire.Hosting/Publishing/PublishingExtensions.cs b/src/Aspire.Hosting/Publishing/PublishingExtensions.cs index 4bf20c4d1f9..31407b9c871 100644 --- a/src/Aspire.Hosting/Publishing/PublishingExtensions.cs +++ b/src/Aspire.Hosting/Publishing/PublishingExtensions.cs @@ -11,6 +11,7 @@ namespace Aspire.Hosting.Pipelines; /// Extension methods for and to provide direct operations. /// [Experimental("ASPIREPIPELINES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] +[AspireExportIgnore(Reason = "Convenience wrapper over pipeline APIs — use the dedicated ATS pipeline exports instead.")] public static class PublishingExtensions { /// diff --git a/src/Aspire.Hosting/ResourceBuilderExtensions.cs b/src/Aspire.Hosting/ResourceBuilderExtensions.cs index 23351cd5469..5a21c7ca334 100644 --- a/src/Aspire.Hosting/ResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ResourceBuilderExtensions.cs @@ -32,7 +32,7 @@ public static class ResourceBuilderExtensions /// The name of the environment variable. /// The value of the environment variable. /// The . - [AspireExport("withEnvironment", Description = "Sets an environment variable")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withEnvironment dispatcher export.")] public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, string? value) where T : IResourceWithEnvironment { ArgumentNullException.ThrowIfNull(builder); @@ -70,27 +70,12 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu /// /// Adds an environment variable to the resource with a reference expression value. /// - /// - /// - /// This overload enables polyglot hosts to set environment variables using dynamic - /// expressions that reference endpoints, parameters, and other value providers. - /// - /// - /// Usage from TypeScript: - /// - /// const redis = await builder.addRedis("cache"); - /// const endpoint = await redis.getEndpoint("tcp"); - /// const expr = refExpr`redis://${endpoint}:6379`; - /// await api.withEnvironmentExpression("REDIS_URL", expr); - /// - /// - /// /// The resource type. /// The resource builder. /// The name of the environment variable. /// A ReferenceExpression that will be evaluated at runtime. /// The . - [AspireExport("withEnvironmentExpression", Description = "Adds an environment variable with a reference expression")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the internal withEnvironment dispatcher export.")] public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, string name, ReferenceExpression value) where T : IResourceWithEnvironment { @@ -132,7 +117,7 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu /// The resource builder. /// A callback that allows for deferred execution for computing many environment variables. This runs after resources have been allocated by the orchestrator and allows access to other resources to resolve computed data, e.g. connection strings, ports. /// The . - [AspireExport("withEnvironmentCallback", Description = "Sets environment variables via callback")] + [AspireExportIgnore(Reason = "Polyglot app hosts use the async callback overload.")] public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, Action callback) where T : IResourceWithEnvironment { ArgumentNullException.ThrowIfNull(builder); @@ -148,7 +133,7 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu /// The resource builder. /// A callback that allows for deferred execution for computing many environment variables. This runs after resources have been allocated by the orchestrator and allows access to other resources to resolve computed data, e.g. connection strings, ports. /// The . - [AspireExport("withEnvironmentCallbackAsync", Description = "Sets environment variables via async callback")] + [AspireExport("withEnvironmentCallback", Description = "Sets environment variables via callback")] public static IResourceBuilder WithEnvironment(this IResourceBuilder builder, Func callback) where T : IResourceWithEnvironment { ArgumentNullException.ThrowIfNull(builder); @@ -181,6 +166,55 @@ public static IResourceBuilder WithEnvironment(this IResourceBuilder bu }); } + /// + /// Adds an environment variable to the resource. + /// + /// The resource type. + /// The resource builder. + /// The name of the environment variable. + /// The value of the environment variable. + /// The . + /// Thrown when , , or is null. + /// Thrown when is not a supported type. + [AspireExport("withEnvironment", Description = "Sets an environment variable on the resource")] + internal static IResourceBuilder WithEnvironment( + this IResourceBuilder builder, + string name, + [AspireUnion(typeof(string), typeof(ReferenceExpression), typeof(EndpointReference), typeof(IResourceBuilder), typeof(IResourceBuilder))] object value) + where T : IResourceWithEnvironment + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(name); + ArgumentNullException.ThrowIfNull(value); + + return value switch + { + string s => builder.WithEnvironment(name, s), + ReferenceExpression expr => builder.WithEnvironment(name, expr), + EndpointReference endpoint => builder.WithEnvironment(name, endpoint), + IResourceBuilder param => builder.WithEnvironment(name, param), + IResourceBuilder connStr => builder.WithEnvironment(name, connStr), + IValueProvider and IManifestExpressionProvider => WithEnvironmentValueProvider(builder, name, value), + _ => throw new ArgumentException( + $"Unsupported value type '{value.GetType().Name}'. Expected string, ReferenceExpression, EndpointReference, ParameterResource, connection string resource, or an IValueProvider.", + nameof(value)) + }; + } + + private static IResourceBuilder WithEnvironmentValueProvider(IResourceBuilder builder, string name, object value) + where T : IResourceWithEnvironment + { + if (value is IValueWithReferences valueWithReferences) + { + WalkAndLinkResourceReferences(builder, valueWithReferences.References); + } + + return builder.WithEnvironment(context => + { + context.EnvironmentVariables[name] = value; + }); + } + /// /// Adds an environment variable to the resource with the URL from the . /// @@ -761,6 +795,7 @@ private static void SplatConnectionProperties(IResourceWithConnectionString reso /// The resource that provides the connection properties. Cannot be null. /// The key of the connection property to retrieve. Cannot be null. /// The value associated with the specified connection property key. + [AspireExport("getConnectionProperty", Description = "Gets a connection property by key")] public static ReferenceExpression GetConnectionProperty(this IResourceWithConnectionString resource, string key) { foreach (var connectionProperty in resource.GetConnectionProperties()) @@ -1569,7 +1604,7 @@ public static IResourceBuilder WithUrlForEndpoint(this IResourceBuilder /// The resource builder to which container files will be copied to. /// The resource which contains the container files to be copied. /// The destination path within the resource's container where the files will be copied. - [AspireExport("publishWithContainerFiles", Description = "Configures the resource to copy container files from the specified source during publishing")] + [AspireExport("publishWithContainerFilesFromResource", MethodName = "publishWithContainerFiles", Description = "Configures the resource to copy container files from the specified source during publishing")] public static IResourceBuilder PublishWithContainerFiles( this IResourceBuilder builder, IResourceBuilder source, @@ -1674,7 +1709,7 @@ public static IResourceBuilder ExcludeFromManifest(this IResourceBuilder /// /// - [AspireExport("waitFor", Description = "Waits for another resource to be ready")] + [AspireExport("waitForResource", MethodName = "waitFor", Description = "Waits for another resource to be ready")] public static IResourceBuilder WaitFor(this IResourceBuilder builder, IResourceBuilder dependency) where T : IResourceWithWaitSupport { ArgumentNullException.ThrowIfNull(builder); @@ -1782,7 +1817,7 @@ private static IResourceBuilder WaitForCore(this IResourceBuilder build /// /// /// - [AspireExport("waitForStart", Description = "Waits for another resource to start")] + [AspireExport("waitForResourceStart", MethodName = "waitForStart", Description = "Waits for another resource to start")] public static IResourceBuilder WaitForStart(this IResourceBuilder builder, IResourceBuilder dependency) where T : IResourceWithWaitSupport { ArgumentNullException.ThrowIfNull(builder); @@ -1926,7 +1961,7 @@ public static IResourceBuilder WithExplicitStart(this IResourceBuilder /// /// /// - [AspireExport("waitForCompletion", Description = "Waits for resource completion")] + [AspireExport("waitForResourceCompletion", MethodName = "waitForCompletion", Description = "Waits for resource completion")] public static IResourceBuilder WaitForCompletion(this IResourceBuilder builder, IResourceBuilder dependency, int exitCode = 0) where T : IResourceWithWaitSupport { ArgumentNullException.ThrowIfNull(builder); @@ -2716,7 +2751,7 @@ public static IResourceBuilder WithCertificateTrustConfiguration /// [Experimental("ASPIRECERTIFICATES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] - [AspireExport("withHttpsDeveloperCertificate", Description = "Configures HTTPS with a developer certificate")] + [AspireExport("withParameterHttpsDeveloperCertificate", MethodName = "withHttpsDeveloperCertificate", Description = "Configures HTTPS with a developer certificate")] public static IResourceBuilder WithHttpsDeveloperCertificate(this IResourceBuilder builder, IResourceBuilder? password = null) where TResource : IResourceWithEnvironment, IResourceWithArgs { @@ -3116,7 +3151,7 @@ public static IResourceBuilder WithReferenceRelationship( /// /// /// - [AspireExport("withParentRelationship", Description = "Sets the parent relationship")] + [AspireExport("withBuilderParentRelationship", MethodName = "withParentRelationship", Description = "Sets the parent relationship")] public static IResourceBuilder WithParentRelationship( this IResourceBuilder builder, IResourceBuilder parent) where T : IResource @@ -3180,7 +3215,7 @@ public static IResourceBuilder WithParentRelationship( /// /// /// - [AspireExport("withChildRelationship", Description = "Sets a child relationship")] + [AspireExport("withBuilderChildRelationship", MethodName = "withChildRelationship", Description = "Sets a child relationship")] public static IResourceBuilder WithChildRelationship( this IResourceBuilder builder, IResourceBuilder child) where T : IResource @@ -3289,6 +3324,7 @@ public static IResourceBuilder WithComputeEnvironment(this IResourceBuilde /// The type of the resource. /// Optional callback to add or modify command line arguments when running in an extension host. Useful if the entrypoint is usually provided as an argument to the resource executable. [Experimental("ASPIREEXTENSION001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExportIgnore(Reason = "Generic debug launch configuration support is not part of the ATS surface.")] public static IResourceBuilder WithDebugSupport(this IResourceBuilder builder, Func launchConfigurationProducer, string launchConfigurationType, Action? argsCallback = null) where T : IResource { @@ -3348,6 +3384,7 @@ public static IResourceBuilder WithDebugSupport(this /// This method is not available in polyglot app hosts. The parameter name 'type' is a reserved keyword in Go and Rust. /// [Experimental("ASPIREPROBES001", UrlFormat = "https://aka.ms/aspire/diagnostics/{0}")] + [AspireExportIgnore(Reason = "Use the ATS export stub with renamed probeType parameter instead.")] public static IResourceBuilder WithHttpProbe(this IResourceBuilder builder, ProbeType type, string? path = null, int? initialDelaySeconds = null, int? periodSeconds = null, int? timeoutSeconds = null, int? failureThreshold = null, int? successThreshold = null, string? endpointName = null) where T : IResourceWithEndpoints, IResourceWithProbes { diff --git a/src/Aspire.Hosting/Utils/ExtensionUtils.cs b/src/Aspire.Hosting/Utils/ExtensionUtils.cs index 62efc6b5f9e..27ea86fb712 100644 --- a/src/Aspire.Hosting/Utils/ExtensionUtils.cs +++ b/src/Aspire.Hosting/Utils/ExtensionUtils.cs @@ -13,6 +13,7 @@ namespace Aspire.Hosting.Utils; #pragma warning disable ASPIREEXTENSION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. internal static class ExtensionUtils { + [AspireExportIgnore(Reason = "Debug support inspection is a local .NET helper and is not part of the ATS surface.")] public static bool SupportsDebugging(this IResource builder, IConfiguration configuration, [NotNullWhen(true)] out SupportsDebuggingAnnotation? supportsDebuggingAnnotation) { var supportedLaunchConfigurations = GetSupportedLaunchConfigurations(configuration); diff --git a/src/Aspire.Managed/NuGet/Commands/RestoreCommand.cs b/src/Aspire.Managed/NuGet/Commands/RestoreCommand.cs index 0507b06af52..72e11447ef0 100644 --- a/src/Aspire.Managed/NuGet/Commands/RestoreCommand.cs +++ b/src/Aspire.Managed/NuGet/Commands/RestoreCommand.cs @@ -144,9 +144,8 @@ private static async Task ExecuteRestoreAsync( var outputPath = Path.GetFullPath(output); Directory.CreateDirectory(outputPath); - packagesDir ??= Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), - ".nuget", "packages"); + var settings = Settings.LoadDefaultSettings(workingDir, nugetConfigPath, new XPlatMachineWideSetting()); + packagesDir ??= SettingsUtility.GetGlobalPackagesFolder(settings); var logger = new NuGetLogger(verbose); diff --git a/src/Aspire.ProjectTemplates/templates/aspire-apphost-singlefile/aspire.config.json b/src/Aspire.ProjectTemplates/templates/aspire-apphost-singlefile/aspire.config.json new file mode 100644 index 00000000000..a777aa51fac --- /dev/null +++ b/src/Aspire.ProjectTemplates/templates/aspire-apphost-singlefile/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "path": "apphost.cs" + } +} diff --git a/src/Aspire.ProjectTemplates/templates/aspire-apphost/aspire.config.json b/src/Aspire.ProjectTemplates/templates/aspire-apphost/aspire.config.json new file mode 100644 index 00000000000..00d04809d13 --- /dev/null +++ b/src/Aspire.ProjectTemplates/templates/aspire-apphost/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "path": "Aspire.AppHost1.csproj" + } +} diff --git a/src/Aspire.ProjectTemplates/templates/aspire-empty/aspire.config.json b/src/Aspire.ProjectTemplates/templates/aspire-empty/aspire.config.json new file mode 100644 index 00000000000..2af258e560b --- /dev/null +++ b/src/Aspire.ProjectTemplates/templates/aspire-empty/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "path": "AspireApplication.1.AppHost/AspireApplication.1.AppHost.csproj" + } +} diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/aspire.config.json b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/aspire.config.json new file mode 100644 index 00000000000..a777aa51fac --- /dev/null +++ b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/aspire.config.json @@ -0,0 +1,5 @@ +{ + "appHost": { + "path": "apphost.cs" + } +} diff --git a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.tsx b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.tsx index b9b5a1b10ae..869f34c5cac 100644 --- a/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.tsx +++ b/src/Aspire.ProjectTemplates/templates/aspire-py-starter/frontend/src/App.tsx @@ -166,7 +166,7 @@ function App() { Learn more about Aspire (opens in new tab) (opens in new tab) GetAspireExportDataAll(Assembly asse // --- AspireExportIgnore lookup --- + /// + /// Determines whether the specified has the AspireExportIgnore attribute. + /// + public static bool HasAspireExportIgnoreData(Type type) + => HasAttribute(type.GetCustomAttributesData(), AspireExportIgnoreAttributeFullName); + /// /// Determines whether the specified has the AspireExportIgnore attribute. /// diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 63c1108cf71..ff3b4ae7799 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -16,7 +16,7 @@ - Socket Naming Format /// /// -/// New format: auxi.sock.{hash}.{pid} +/// New format: auxi.sock.{appHostHash}.{instanceHash}.{pid} /// /// /// auxi.sock - Prefix (not "aux" because that's reserved on Windows) -/// {hash} - SHA256(AppHost project path)[0:16] - identifies the AppHost project +/// {appHostHash} - xxHash(AppHost project path)[0:16] - identifies the AppHost project +/// {instanceHash} - random hex identifier[0:12] - makes each socket name non-deterministic /// {pid} - Process ID of the AppHost - identifies the specific instance /// /// -/// Old format (for backward compatibility): auxi.sock.{hash} +/// Previous format (for backward compatibility): auxi.sock.{appHostHash}.{pid} +/// +/// +/// Old format (for backward compatibility): auxi.sock.{appHostHash} /// /// /// Why PID in the Filename? @@ -72,16 +77,30 @@ internal static class BackchannelConstants public const string SocketPrefix = "auxi.sock"; /// - /// Number of hex characters to use from the SHA256 hash. + /// Number of hex characters to use from the stable xxHash-based AppHost identifier. /// /// /// Using 16 chars (64 bits) balances uniqueness against path length constraints. /// Unix socket paths are limited to ~104 characters on most systems. - /// Full path example: ~/.aspire/cli/backchannels/auxi.sock.bc43b855b6848166.46730 - /// = ~65 characters, well under the limit. + /// Full path example: ~/.aspire/cli/backchannels/auxi.sock.bc43b855b6848166.a1b2c3d4e5f6.46730 + /// = ~78 characters, well under the limit. /// public const int HashLength = 16; + /// + /// Number of hex characters to use for compact local identifiers. + /// + /// + /// Using 12 chars (48 bits) keeps socket and package cache paths short while still providing + /// enough variation for local file names that are not part of a security boundary. + /// + public const int CompactIdentifierLength = 12; + + /// + /// Number of hex characters to use from the randomized instance identifier. + /// + public const int InstanceHashLength = CompactIdentifierLength; + /// /// Gets the backchannels directory path for the given home directory. /// @@ -103,16 +122,15 @@ public static string GetBackchannelsDirectory(string homeDirectory) /// A 16-character lowercase hex string. public static string ComputeHash(string appHostPath) { - var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(appHostPath)); - return Convert.ToHexString(hashBytes)[..HashLength].ToLowerInvariant(); + return ComputeStableIdentifier(appHostPath, HashLength); } /// /// Computes the full socket path for an AppHost instance. /// /// - /// Called by AppHost when creating the socket. Includes the PID to ensure - /// uniqueness across multiple instances of the same AppHost. + /// Called by AppHost when creating the socket. Includes a randomized instance hash and the PID + /// to ensure uniqueness across multiple instances of the same AppHost. /// /// The full path to the AppHost project file. /// The user's home directory. @@ -122,7 +140,8 @@ public static string ComputeSocketPath(string appHostPath, string homeDirectory, { var dir = GetBackchannelsDirectory(homeDirectory); var hash = ComputeHash(appHostPath); - return Path.Combine(dir, $"{SocketPrefix}.{hash}.{processId}"); + var instanceHash = CreateRandomIdentifier(InstanceHashLength); + return Path.Combine(dir, $"{SocketPrefix}.{hash}.{instanceHash}.{processId}"); } /// @@ -147,7 +166,8 @@ public static string ComputeSocketPrefix(string appHostPath, string homeDirector /// /// /// Returns all socket files for an AppHost, regardless of PID. This includes - /// both old format (auxi.sock.{hash}) and new format (auxi.sock.{hash}.{pid}). + /// old format (auxi.sock.{hash}), previous format (auxi.sock.{hash}.{pid}), + /// and current format (auxi.sock.{hash}.{instanceHash}.{pid}). /// /// The full path to the AppHost project file. /// The user's home directory. @@ -163,13 +183,15 @@ public static string[] FindMatchingSockets(string appHostPath, string homeDirect return []; } - // Match both old format (auxi.sock.{hash}) and new format (auxi.sock.{hash}.{pid}) + // Match old format (auxi.sock.{hash}), previous format (auxi.sock.{hash}.{pid}), + // and current format (auxi.sock.{hash}.{instanceHash}.{pid}) // Use pattern with "*" to match optional PID suffix var allMatches = Directory.GetFiles(dir, prefixFileName + "*"); - // Filter to only include exact match (old format) or .{pid} suffix (new format) - // This avoids matching auxi.sock.{hash}abc (different hash that starts with same chars) - // and also avoids matching files like auxi.sock.{hash}.12345.bak + // Filter to only include exact match (old format), .{pid} suffix (previous format), + // or .{instanceHash}.{pid} suffix (current format). This avoids matching + // auxi.sock.{hash}abc (different hash that starts with same chars) and files + // like auxi.sock.{hash}.12345.bak. return allMatches.Where(f => { var fileName = Path.GetFileName(f); @@ -177,12 +199,24 @@ public static string[] FindMatchingSockets(string appHostPath, string homeDirect { return true; // Old format: exact match } - if (fileName.StartsWith(prefixFileName + ".", StringComparison.Ordinal) && - int.TryParse(fileName.AsSpan(prefixFileName.Length + 1), NumberStyles.None, CultureInfo.InvariantCulture, out _)) + + if (!fileName.StartsWith(prefixFileName + ".", StringComparison.Ordinal)) { - return true; // New format: prefix followed by dot and integer PID + return false; } - return false; + + var suffix = fileName[(prefixFileName.Length + 1)..]; + var segments = suffix.Split('.'); + + if (segments.Length == 1 && + int.TryParse(segments[0], NumberStyles.None, CultureInfo.InvariantCulture, out _)) + { + return true; // Previous format: prefix followed by integer PID + } + + return segments.Length == 2 && + IsHex(segments[0]) && + int.TryParse(segments[1], NumberStyles.None, CultureInfo.InvariantCulture, out _); }).ToArray(); } @@ -190,7 +224,8 @@ public static string[] FindMatchingSockets(string appHostPath, string homeDirect /// Extracts the hash from a socket filename. /// /// - /// Works with both old format (auxi.sock.{hash}) and new format (auxi.sock.{hash}.{pid}). + /// Works with old format (auxi.sock.{hash}), previous format (auxi.sock.{hash}.{pid}), + /// and current format (auxi.sock.{hash}.{instanceHash}.{pid}). /// /// The full socket path or filename. /// The hash portion, or null if the format is unrecognized. @@ -198,12 +233,13 @@ public static string[] FindMatchingSockets(string appHostPath, string homeDirect { var fileName = Path.GetFileName(socketPath); - // Handle new format: auxi.sock.{hash}.{pid} + // Handle current format: auxi.sock.{hash}.{instanceHash}.{pid} + // Handle previous format: auxi.sock.{hash}.{pid} // Handle old format: auxi.sock.{hash} if (fileName.StartsWith($"{SocketPrefix}.", StringComparison.Ordinal)) { var afterPrefix = fileName[($"{SocketPrefix}.".Length)..]; - // If there's another dot, it's new format - return just the hash part + // If there's another dot, it's a multi-segment format - return just the AppHost hash part var dotIndex = afterPrefix.IndexOf('.'); return dotIndex > 0 ? afterPrefix[..dotIndex] : afterPrefix; } @@ -220,7 +256,7 @@ public static string[] FindMatchingSockets(string appHostPath, string homeDirect } /// - /// Extracts the PID from a socket filename (new format only). + /// Extracts the PID from a socket filename when one is present. /// /// The full socket path or filename. /// The PID if present and valid, or null for old format sockets. @@ -273,7 +309,8 @@ public static bool ProcessExists(int pid) /// support PID-based orphan detection. /// /// - /// Limitation: This method only cleans up new format sockets (auxi.sock.{hash}.{pid}) + /// Limitation: This method only cleans up sockets that include a PID + /// (auxi.sock.{hash}.{pid} or auxi.sock.{hash}.{instanceHash}.{pid}) /// because old format sockets (auxi.sock.{hash}) don't have a PID for orphan detection. /// Old format sockets are cleaned up via connection-based detection in the CLI. /// @@ -291,7 +328,7 @@ public static int CleanupOrphanedSockets(string backchannelsDirectory, string ha return deleted; } - // Find all sockets for this hash (both old and new format) + // Find all sockets for this hash across all supported formats. var pattern = $"{SocketPrefix}.{hash}*"; foreach (var socketPath in Directory.GetFiles(backchannelsDirectory, pattern)) { @@ -317,4 +354,48 @@ public static int CleanupOrphanedSockets(string backchannelsDirectory, string ha return deleted; } + + /// + /// Computes a compact stable identifier from a string value. + /// + /// + /// Uses XxHash3 because these identifiers are only used for local naming and lookup. They do not + /// protect secrets or cross a trust boundary, so a fast non-cryptographic hash is preferable to SHA-2. + /// + /// The string value to hash. + /// The number of lowercase hex characters to return. + /// A lowercase hex identifier truncated to characters. + public static string ComputeStableIdentifier(string value, int length = CompactIdentifierLength) + { + ArgumentException.ThrowIfNullOrEmpty(value); + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length); + var xxHash = new XxHash3(); + xxHash.Append(Encoding.UTF8.GetBytes(value)); + + return ToLowerHexIdentifier(xxHash.GetCurrentHash(), length); + } + + /// + /// Creates a compact randomized identifier. + /// + /// The number of lowercase hex characters to return. + /// A lowercase hex identifier truncated to characters. + public static string CreateRandomIdentifier(int length = CompactIdentifierLength) + { + ArgumentOutOfRangeException.ThrowIfNegativeOrZero(length); + + Span randomBytes = stackalloc byte[(length + 1) / 2]; + RandomNumberGenerator.Fill(randomBytes); + + return ToLowerHexIdentifier(randomBytes, length); + } + + private static bool IsHex(string value) + => !string.IsNullOrEmpty(value) && value.All(static c => char.IsAsciiHexDigit(c)); + + private static string ToLowerHexIdentifier(ReadOnlySpan bytes, int length) + { + var hex = Convert.ToHexString(bytes).ToLowerInvariant(); + return hex[..Math.Min(length, hex.Length)]; + } } diff --git a/src/Shared/IConfigurationExtensions.cs b/src/Shared/IConfigurationExtensions.cs index ffaf4f1b5b8..0d085511087 100644 --- a/src/Shared/IConfigurationExtensions.cs +++ b/src/Shared/IConfigurationExtensions.cs @@ -2,10 +2,22 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Diagnostics.CodeAnalysis; +#if !CLI && !ASPIRE_DASHBOARD +using Aspire.Hosting; +#endif using Microsoft.Extensions.Configuration; namespace Aspire; +#if CLI || ASPIRE_DASHBOARD +[AttributeUsage(AttributeTargets.All)] +internal sealed class AspireExportIgnoreAttribute : Attribute +{ + public string? Reason { get; set; } +} +#endif + +[AspireExportIgnore(Reason = "Internal IConfiguration helper — use the dedicated ATS configuration exports instead.")] internal static class IConfigurationExtensions { #if !CLI diff --git a/src/Shared/KnownConfigNames.cs b/src/Shared/KnownConfigNames.cs index 57d3f2dbe54..5ef33693d5d 100644 --- a/src/Shared/KnownConfigNames.cs +++ b/src/Shared/KnownConfigNames.cs @@ -30,6 +30,7 @@ internal static class KnownConfigNames public const string WaitForDebugger = "ASPIRE_WAIT_FOR_DEBUGGER"; public const string WaitForDebuggerTimeout = "ASPIRE_DEBUGGER_TIMEOUT"; public const string UnixSocketPath = "ASPIRE_BACKCHANNEL_PATH"; + public const string RemoteAppHostToken = "ASPIRE_REMOTE_APPHOST_TOKEN"; public const string CliProcessId = "ASPIRE_CLI_PID"; public const string CliProcessStarted = "ASPIRE_CLI_STARTED"; public const string ForceRichConsole = "ASPIRE_FORCE_RICH_CONSOLE"; diff --git a/src/Shared/PathNormalizer.cs b/src/Shared/PathNormalizer.cs index 419ad899e02..090657f20ba 100644 --- a/src/Shared/PathNormalizer.cs +++ b/src/Shared/PathNormalizer.cs @@ -17,4 +17,13 @@ public static string NormalizePathForCurrentPlatform(string path) return Path.GetFullPath(path); } + + /// + /// Normalizes a path for storage in configuration files by replacing + /// backslash separators with forward slashes. + /// + public static string NormalizePathForStorage(string path) + { + return path.Replace('\\', '/'); + } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/ConfigDiscoveryTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/ConfigDiscoveryTests.cs new file mode 100644 index 00000000000..d7d5f1b7a0f --- /dev/null +++ b/tests/Aspire.Cli.EndToEnd.Tests/ConfigDiscoveryTests.cs @@ -0,0 +1,149 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; +using Aspire.Cli.EndToEnd.Tests.Helpers; +using Aspire.Cli.Tests.Utils; +using Hex1b.Automation; +using Hex1b.Input; +using Xunit; + +namespace Aspire.Cli.EndToEnd.Tests; + +/// +/// End-to-end tests verifying that aspire.config.json is discovered from the +/// apphost's directory rather than being recreated in the current working directory. +/// +/// +/// Reproduces the bug where aspire new myproject creates the config inside +/// myproject/, but running aspire run from the parent directory +/// creates a spurious aspire.config.json in the parent instead of finding +/// the one adjacent to apphost.ts. +/// +public sealed class ConfigDiscoveryTests(ITestOutputHelper output) +{ + /// + /// Verifies that running aspire run from a parent directory discovers the + /// existing aspire.config.json next to the apphost rather than creating a + /// new one in the current working directory. + /// + [Fact] + public async Task RunFromParentDirectory_UsesExistingConfigNearAppHost() + { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); + + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal( + repoRoot, installMode, output, + variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, + mountDockerSocket: true, + workspace: workspace); + + var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); + + var counter = new SequenceCounter(); + var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + + await auto.PrepareDockerEnvironmentAsync(counter, workspace); + await auto.InstallAspireCliInDockerAsync(installMode, counter); + + const string projectName = "ConfigTest"; + + // Step 1: Create a TypeScript Empty AppHost project. + // This creates a subdirectory with aspire.config.json inside it. + await auto.AspireNewAsync(projectName, counter, template: AspireTemplate.TypeScriptEmptyAppHost); + + // Capture the original config content before running from the parent directory. + var projectConfigPath = Path.Combine( + workspace.WorkspaceRoot.FullName, projectName, "aspire.config.json"); + var parentConfigPath = Path.Combine( + workspace.WorkspaceRoot.FullName, "aspire.config.json"); + + // Verify the project config was created by aspire new + Assert.True(File.Exists(projectConfigPath), + $"aspire new should have created {projectConfigPath}"); + + var originalContent = File.ReadAllText(projectConfigPath); + + // Step 2: Stay in the parent directory (do NOT cd into the project). + // Run aspire run — this should find the apphost in the subdirectory + // and use the adjacent aspire.config.json, not create a new one in CWD. + // Run aspire run — this should find the apphost in the subdirectory + // and use the adjacent aspire.config.json, not create a new one in CWD. + await auto.TypeAsync($"aspire run --apphost {projectName}"); + await auto.EnterAsync(); + + // Wait for the run to start (or fail) — either way the config discovery has happened. + await auto.WaitUntilAsync(s => + { + // If a "Select an apphost" prompt appears, the bug may have caused multiple detection + if (s.ContainsText("Select an apphost to use:")) + { + throw new InvalidOperationException("Multiple apphosts incorrectly detected"); + } + + return s.ContainsText("Press CTRL+C to stop the apphost and exit.") + || s.ContainsText("ERR:"); + }, timeout: TimeSpan.FromMinutes(3), description: "aspire run started or errored"); + + // Stop the apphost + await auto.Ctrl().KeyAsync(Hex1bKey.C); + await auto.WaitForAnyPromptAsync(counter, timeout: TimeSpan.FromSeconds(30)); + + // Step 3: Assertions on file system state (host-side via bind mount). + + // The parent directory should NOT have an aspire.config.json. + Assert.False(File.Exists(parentConfigPath), + $"aspire.config.json should NOT be created in the parent/CWD directory. " + + $"Found: {parentConfigPath}"); + + // The project's aspire.config.json should still exist with its original rich content. + Assert.True(File.Exists(projectConfigPath), + $"aspire.config.json in project directory should still exist: {projectConfigPath}"); + + var currentContent = File.ReadAllText(projectConfigPath); + + // Verify the config was not modified by the run. + Assert.Equal(originalContent, currentContent); + + using var doc = JsonDocument.Parse(currentContent); + var root = doc.RootElement; + + // Verify appHost.path is "apphost.ts" + Assert.True(root.TryGetProperty("appHost", out var appHost), + $"aspire.config.json missing 'appHost' property. Content:\n{currentContent}"); + Assert.True(appHost.TryGetProperty("path", out var pathProp), + $"aspire.config.json missing 'appHost.path'. Content:\n{currentContent}"); + Assert.Equal("apphost.ts", pathProp.GetString()); + + // Verify language is typescript + Assert.True(appHost.TryGetProperty("language", out var langProp), + $"aspire.config.json missing 'appHost.language'. Content:\n{currentContent}"); + Assert.Contains("typescript", langProp.GetString(), StringComparison.OrdinalIgnoreCase); + + // Verify profiles section exists with applicationUrl + Assert.True(root.TryGetProperty("profiles", out var profiles), + $"aspire.config.json missing 'profiles' section. Content:\n{currentContent}"); + Assert.True(profiles.EnumerateObject().Any(), + $"aspire.config.json 'profiles' section is empty. Content:\n{currentContent}"); + + // At least one profile should have an applicationUrl + var hasApplicationUrl = false; + foreach (var profile in profiles.EnumerateObject()) + { + if (profile.Value.TryGetProperty("applicationUrl", out _)) + { + hasApplicationUrl = true; + break; + } + } + Assert.True(hasApplicationUrl, + $"No profile has 'applicationUrl'. Content:\n{currentContent}"); + + await auto.TypeAsync("exit"); + await auto.EnterAsync(); + + await pendingRun; + } +} diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs index e4363bab2c4..55e016d5ab4 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2EAutomatorHelpers.cs @@ -147,23 +147,34 @@ internal static async Task SourceAspireCliEnvironmentAsync( } /// - /// Verifies the installed Aspire CLI version matches the expected commit SHA. + /// Verifies the installed Aspire CLI version matches the expected build. + /// Always checks the dynamic version prefix from eng/Versions.props. + /// For non-stabilized builds (all normal PR builds), also verifies the commit SHA suffix. /// internal static async Task VerifyAspireCliVersionAsync( this Hex1bTerminalAutomator auto, string commitSha, SequenceCounter counter) { - if (commitSha.Length != 40) - { - throw new ArgumentException($"Commit SHA must be exactly 40 characters, got {commitSha.Length}: '{commitSha}'", nameof(commitSha)); - } + var versionPrefix = CliE2ETestHelpers.GetVersionPrefix(); + var isStabilized = CliE2ETestHelpers.IsStabilizedBuild(); - var shortCommitSha = commitSha[..8]; - var expectedVersionSuffix = $"g{shortCommitSha}"; await auto.TypeAsync("aspire --version"); await auto.EnterAsync(); - await auto.WaitUntilTextAsync(expectedVersionSuffix, timeout: TimeSpan.FromSeconds(10)); + + // Always verify the version prefix matches the branch's version (e.g., "13.3.0"). + await auto.WaitUntilTextAsync(versionPrefix, timeout: TimeSpan.FromSeconds(10)); + + // For non-stabilized builds (all PR CI builds), also verify the commit SHA suffix + // to uniquely identify the exact build. Stabilized builds (official releases only) + // produce versions without SHA suffixes, so we skip this check. + if (!isStabilized && commitSha.Length == 40) + { + var shortCommitSha = commitSha[..8]; + var expectedVersionSuffix = $"g{shortCommitSha}"; + await auto.WaitUntilTextAsync(expectedVersionSuffix, timeout: TimeSpan.FromSeconds(10)); + } + await auto.WaitForSuccessPromptAsync(counter); } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs index ed41c1a899b..2937fec507c 100644 --- a/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs +++ b/tests/Aspire.Cli.EndToEnd.Tests/Helpers/CliE2ETestHelpers.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Runtime.CompilerServices; +using System.Xml.Linq; using Aspire.Cli.Tests.Utils; using Hex1b; using Xunit; @@ -309,4 +310,51 @@ internal static string ToContainerPath(string hostPath, TemporaryWorkspace works var relativePath = Path.GetRelativePath(workspace.WorkspaceRoot.FullName, hostPath); return $"/workspace/{workspace.WorkspaceRoot.Name}/" + relativePath.Replace('\\', '/'); } + + /// + /// Reads the VersionPrefix (e.g., "13.3.0") from eng/Versions.props by parsing + /// the MajorVersion, MinorVersion, and PatchVersion MSBuild properties. + /// + internal static string GetVersionPrefix() + { + var repoRoot = GetRepoRoot(); + var versionsPropsPath = Path.Combine(repoRoot, "eng", "Versions.props"); + + var doc = XDocument.Load(versionsPropsPath); + var ns = doc.Root?.Name.Namespace ?? XNamespace.None; + + string? GetProperty(string name) => + doc.Descendants(ns + name).FirstOrDefault()?.Value; + + var major = GetProperty("MajorVersion") + ?? throw new InvalidOperationException("MajorVersion not found in eng/Versions.props"); + var minor = GetProperty("MinorVersion") + ?? throw new InvalidOperationException("MinorVersion not found in eng/Versions.props"); + var patch = GetProperty("PatchVersion") + ?? throw new InvalidOperationException("PatchVersion not found in eng/Versions.props"); + + return $"{major}.{minor}.{patch}"; + } + + /// + /// Checks whether the build is stabilized (StabilizePackageVersion=true in eng/Versions.props). + /// Stabilized builds produce version strings without commit SHA suffixes (e.g., "13.2.0" instead + /// of "13.2.0-preview.1.25175.1+g{sha}"). This is only true for official release builds, + /// never for normal PR CI builds. + /// + internal static bool IsStabilizedBuild() + { + var repoRoot = GetRepoRoot(); + var versionsPropsPath = Path.Combine(repoRoot, "eng", "Versions.props"); + + var doc = XDocument.Load(versionsPropsPath); + var ns = doc.Root?.Name.Namespace ?? XNamespace.None; + + // The default value in Versions.props uses a Condition to default to "false", + // so we read the element's text directly. + var stabilize = doc.Descendants(ns + "StabilizePackageVersion") + .FirstOrDefault()?.Value; + + return string.Equals(stabilize, "true", StringComparison.OrdinalIgnoreCase); + } } diff --git a/tests/Aspire.Cli.EndToEnd.Tests/LocalConfigMigrationTests.cs b/tests/Aspire.Cli.EndToEnd.Tests/LocalConfigMigrationTests.cs new file mode 100644 index 00000000000..ffbf5300d36 --- /dev/null +++ b/tests/Aspire.Cli.EndToEnd.Tests/LocalConfigMigrationTests.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.EndToEnd.Tests.Helpers; +using Aspire.Cli.Tests.Utils; +using Hex1b.Automation; +using Hex1b.Input; +using Xunit; + +namespace Aspire.Cli.EndToEnd.Tests; + +/// +/// End-to-end tests verifying that legacy .aspire/settings.json files with relative paths +/// are correctly migrated to aspire.config.json with adjusted paths. +/// +/// +/// When .aspire/settings.json stores appHostPath relative to the .aspire/ directory +/// (e.g., "../apphost.ts"), the migration to aspire.config.json must re-base the path +/// to be relative to the config file's own directory (e.g., "apphost.ts"). +/// +public sealed class LocalConfigMigrationTests(ITestOutputHelper output) +{ + /// + /// Verifies that migrating a legacy .aspire/settings.json with a "../apphost.ts" path + /// produces an aspire.config.json with the correct "apphost.ts" relative path. + /// + /// + /// + /// This scenario reproduces the bug where FromLegacy() copied appHostPath verbatim, + /// without adjusting from .aspire/-relative to project-root-relative. + /// + /// + /// The test keeps the TS project intact (apphost.ts at root with .modules/) so that + /// aspire run can actually start successfully. The re-basing logic "../apphost.ts" → + /// "apphost.ts" exercises the same code path as "../src/apphost.ts" → "src/apphost.ts". + /// + /// + [Fact] + public async Task LegacySettingsMigration_AdjustsRelativeAppHostPath() + { + var repoRoot = CliE2ETestHelpers.GetRepoRoot(); + var installMode = CliE2ETestHelpers.DetectDockerInstallMode(repoRoot); + var workspace = TemporaryWorkspace.Create(output); + + using var terminal = CliE2ETestHelpers.CreateDockerTestTerminal( + repoRoot, installMode, output, + variant: CliE2ETestHelpers.DockerfileVariant.Polyglot, + mountDockerSocket: true, + workspace: workspace); + + var pendingRun = terminal.RunAsync(TestContext.Current.CancellationToken); + + var counter = new SequenceCounter(); + var auto = new Hex1bTerminalAutomator(terminal, defaultTimeout: TimeSpan.FromSeconds(500)); + + await auto.PrepareDockerEnvironmentAsync(counter, workspace); + await auto.InstallAspireCliInDockerAsync(installMode, counter); + + // Step 1: Create a valid TypeScript AppHost using aspire init. + // This produces apphost.ts, .modules/, aspire.config.json, etc. + await auto.TypeAsync("aspire init"); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("Which language would you like to use?", timeout: TimeSpan.FromSeconds(30)); + await auto.DownAsync(); + await auto.WaitUntilTextAsync("> TypeScript (Node.js)", timeout: TimeSpan.FromSeconds(5)); + await auto.EnterAsync(); + await auto.WaitUntilTextAsync("Created apphost.ts", timeout: TimeSpan.FromMinutes(2)); + await auto.DeclineAgentInitPromptAsync(counter); + + // Step 2: Replace aspire.config.json with a legacy .aspire/settings.json. + // The legacy format stores appHostPath relative to the .aspire/ directory, + // so "../apphost.ts" points up from .aspire/ to the workspace root where + // apphost.ts lives. The project files stay in place so aspire run can work. + await auto.TypeAsync("rm -f aspire.config.json"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + var legacySettingsJson = """{"appHostPath":"../apphost.ts","language":"typescript/nodejs","sdkVersion":"13.2.0","channel":"staging"}"""; + await auto.TypeAsync($"mkdir -p .aspire && echo '{legacySettingsJson}' > .aspire/settings.json"); + await auto.EnterAsync(); + await auto.WaitForSuccessPromptAsync(counter); + + // Step 3: Run aspire run to trigger the migration from .aspire/settings.json + // to aspire.config.json. The migration happens during apphost discovery, + // before the actual build/run step. + await auto.TypeAsync("aspire run"); + await auto.EnterAsync(); + + // The migration creates aspire.config.json during apphost discovery, before + // the actual run. Poll the host-side filesystem via the bind mount rather + // than parsing terminal output, which is fragile across different failure modes. + var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, "aspire.config.json"); + var deadline = DateTime.UtcNow.AddMinutes(3); + while (!File.Exists(configPath) && DateTime.UtcNow < deadline) + { + await Task.Delay(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); + } + + // Stop the apphost if it's still running (Ctrl+C is safe even if already exited) + await auto.Ctrl().KeyAsync(Hex1bKey.C); + await auto.WaitForAnyPromptAsync(counter, timeout: TimeSpan.FromSeconds(30)); + + // Step 4: Verify aspire.config.json was created with the corrected path. + // The path should be "apphost.ts" (relative to workspace root), + // NOT "../apphost.ts" (the legacy .aspire/-relative path). + Assert.True(File.Exists(configPath), "aspire.config.json was not created by migration"); + var content = File.ReadAllText(configPath); + Assert.DoesNotContain("\"../apphost.ts\"", content); + Assert.Contains("\"apphost.ts\"", content); + + await auto.TypeAsync("exit"); + await auto.EnterAsync(); + + await pendingRun; + } +} diff --git a/tests/Aspire.Cli.Tests/Commands/CacheCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/CacheCommandTests.cs index 53929cf7da7..694fa455807 100644 --- a/tests/Aspire.Cli.Tests/Commands/CacheCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/CacheCommandTests.cs @@ -38,4 +38,120 @@ public async Task CacheCommandWithHelpArgumentReturnsZero() var exitCode = await result.InvokeAsync().DefaultTimeout(); Assert.Equal(0, exitCode); } + + [Fact] + public async Task CacheClear_ClearsPackagesDirectory() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var packagesDir = new DirectoryInfo(Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "packages")); + var restoreDir = packagesDir.CreateSubdirectory("restore").CreateSubdirectory("ABC123"); + File.WriteAllText(Path.Combine(restoreDir.FullName, "test.dll"), "fake"); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.PackagesDirectory = packagesDir; + }); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("cache clear"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + Assert.False(File.Exists(Path.Combine(restoreDir.FullName, "test.dll"))); + } + + [Fact] + public async Task CacheClear_HandlesNonExistentPackagesDirectory() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var packagesDir = new DirectoryInfo(Path.Combine(workspace.WorkspaceRoot.FullName, ".aspire", "packages-nonexistent")); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.PackagesDirectory = packagesDir; + }); + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("cache clear"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.Equal(ExitCodeConstants.Success, exitCode); + } + + [Fact] + public void ClearDirectoryContents_DeletesFilesAndSubdirectories() + { + var tempDir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"aspire-test-{Guid.NewGuid():N}")); + try + { + tempDir.Create(); + var subDir = tempDir.CreateSubdirectory("nested"); + File.WriteAllText(Path.Combine(tempDir.FullName, "root.txt"), "root"); + File.WriteAllText(Path.Combine(subDir.FullName, "nested.txt"), "nested"); + + var deleted = CacheCommand.ClearCommand.ClearDirectoryContents(tempDir); + + Assert.Equal(2, deleted); + Assert.Empty(tempDir.GetFiles("*", SearchOption.AllDirectories)); + Assert.Empty(tempDir.GetDirectories()); + } + finally + { + if (tempDir.Exists) + { + tempDir.Delete(recursive: true); + } + } + } + + [Fact] + public void ClearDirectoryContents_RespectsSkipPredicate() + { + var tempDir = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"aspire-test-{Guid.NewGuid():N}")); + try + { + tempDir.Create(); + var keepFile = Path.Combine(tempDir.FullName, "keep.txt"); + var deleteFile = Path.Combine(tempDir.FullName, "delete.txt"); + File.WriteAllText(keepFile, "keep"); + File.WriteAllText(deleteFile, "delete"); + + var deleted = CacheCommand.ClearCommand.ClearDirectoryContents( + tempDir, + skipFile: f => f.Name == "keep.txt"); + + Assert.Equal(1, deleted); + Assert.True(File.Exists(keepFile)); + Assert.False(File.Exists(deleteFile)); + } + finally + { + if (tempDir.Exists) + { + tempDir.Delete(recursive: true); + } + } + } + + [Fact] + public void ClearDirectoryContents_ReturnsZero_WhenDirectoryDoesNotExist() + { + var nonExistent = new DirectoryInfo(Path.Combine(Path.GetTempPath(), $"aspire-test-nonexistent-{Guid.NewGuid():N}")); + + var deleted = CacheCommand.ClearCommand.ClearDirectoryContents(nonExistent); + + Assert.Equal(0, deleted); + } + + [Fact] + public void ClearDirectoryContents_ReturnsZero_WhenNull() + { + var deleted = CacheCommand.ClearCommand.ClearDirectoryContents(null); + + Assert.Equal(0, deleted); + } } diff --git a/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs index a671db89691..53998e37f4f 100644 --- a/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/InitCommandTests.cs @@ -1,10 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using Aspire.Cli.Commands; using Aspire.Cli.Interaction; using Aspire.Cli.NuGet; using Aspire.Cli.Packaging; +using Aspire.Cli.Projects; +using Aspire.Cli.Resources; +using Aspire.Cli.Scaffolding; using Aspire.Cli.Tests.TestServices; using Aspire.Cli.Tests.Utils; using Microsoft.Extensions.DependencyInjection; @@ -543,6 +547,109 @@ public async Task InitCommandWithInvalidChannelShowsError() Assert.NotEqual(0, exitCode); } + [Fact] + public async Task InitCommand_WhenCSharpInitializationFails_DisplaysCreationErrorMessage() + { + TestInteractionService? testInteractionService = null; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + + // Create a solution file only (no project files in the same directory) + var solutionFile = new FileInfo(Path.Combine(workspace.WorkspaceRoot.FullName, "Test.sln")); + File.WriteAllText(solutionFile.FullName, "Fake solution file"); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = (sp) => + { + testInteractionService = new TestInteractionService(); + return testInteractionService; + }; + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + runner.GetSolutionProjectsAsyncCallback = (_, _, _) => + { + return (0, Array.Empty()); + }; + runner.NewProjectAsyncCallback = (templateName, projectName, outputPath, invocationOptions, ct) => + { + return 1; // Simulate failure for C# template + }; + return runner; + }; + options.PackagingServiceFactory = (sp) => + { + return new TestPackagingService(); + }; + }); + + var serviceProvider = services.BuildServiceProvider(); + var initCommand = serviceProvider.GetRequiredService(); + + var parseResult = initCommand.Parse("init"); + var exitCode = await parseResult.InvokeAsync().DefaultTimeout(); + + var executionContext = serviceProvider.GetRequiredService(); + var expectedMessage = string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ProjectCouldNotBeCreated, executionContext.LogFilePath); + + Assert.NotEqual(0, exitCode); + Assert.NotNull(testInteractionService); + Assert.Contains(expectedMessage, testInteractionService.DisplayedErrors); + } + + [Fact] + public async Task InitCommand_WhenTypeScriptInitializationFails_DisplaysCreationErrorMessage() + { + TestInteractionService? testInteractionService = null; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = (sp) => + { + testInteractionService = new TestInteractionService(); + return testInteractionService; + }; + + options.LanguageServiceFactory = (sp) => + { + var projectFactory = sp.GetRequiredService(); + var tsProject = projectFactory.GetProject(new LanguageInfo( + LanguageId: new LanguageId(KnownLanguageId.TypeScript), + DisplayName: "TypeScript (Node.js)", + PackageName: "@aspire/app-host", + DetectionPatterns: ["apphost.ts"], + CodeGenerator: "typescript", + AppHostFileName: "apphost.ts")); + return new TestLanguageService { DefaultProject = tsProject }; + }; + }); + + services.AddSingleton(new TestScaffoldingService + { + ScaffoldAsyncCallback = (context, cancellationToken) => + { + return Task.FromResult(false); // Simulate failure for TypeScript scaffolding + } + }); + + var serviceProvider = services.BuildServiceProvider(); + var initCommand = serviceProvider.GetRequiredService(); + + var parseResult = initCommand.Parse("init"); + var exitCode = await parseResult.InvokeAsync().DefaultTimeout(); + + var executionContext = serviceProvider.GetRequiredService(); + var expectedMessage = string.Format(CultureInfo.CurrentCulture, InteractionServiceStrings.ProjectCouldNotBeCreated, executionContext.LogFilePath); + + Assert.NotEqual(0, exitCode); + Assert.NotNull(testInteractionService); + Assert.Contains(expectedMessage, testInteractionService.DisplayedErrors); + } + private sealed class TestPackagingServiceWithChannelTracking(Action onChannelUsed) : IPackagingService { public Task> GetChannelsAsync(CancellationToken cancellationToken = default) diff --git a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs index d671a48055d..6a2ff34ba31 100644 --- a/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs +++ b/tests/Aspire.Cli.Tests/Commands/NewCommandTests.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Globalization; using Aspire.Cli.Utils; using Aspire.Cli.Certificates; using Aspire.Cli.Commands; @@ -1421,8 +1422,8 @@ public async Task NewCommandWithTypeScriptStarterReturnsFailedToBuildArtifactsWh var exitCode = await result.InvokeAsync().DefaultTimeout(); Assert.Equal(ExitCodeConstants.FailedToBuildArtifacts, exitCode); - Assert.Single(interactionService.DisplayedErrors); - Assert.Equal("Automatic 'aspire restore' failed for the new TypeScript starter project. Run 'aspire restore' in the project directory for more details.", interactionService.DisplayedErrors[0]); + Assert.Collection(interactionService.DisplayedErrors, + error => Assert.Equal("Automatic 'aspire restore' failed for the new TypeScript starter project. Run 'aspire restore' in the project directory for more details.", error)); } [Fact] @@ -1471,6 +1472,117 @@ public async Task NewCommandNonInteractiveDoesNotPrompt() var exitCode = await result.InvokeAsync().DefaultTimeout(); Assert.Equal(0, exitCode); } + + [Fact] + public async Task NewCommand_WhenCSharpTemplateApplyFails_DisplaysCreationErrorMessage() + { + TestInteractionService? testInteractionService = null; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = (sp) => + { + testInteractionService = new TestInteractionService(); + return testInteractionService; + }; + + options.NewCommandPrompterFactory = (sp) => + { + var interactionService = sp.GetRequiredService(); + return new TestNewCommandPrompter(interactionService); + }; + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + { + var package = new NuGetPackage() + { + Id = "Aspire.ProjectTemplates", + Source = "nuget", + Version = "9.2.0" + }; + + return (0, new NuGetPackage[] { package }); + }; + + runner.NewProjectAsyncCallback = (templateName, projectName, outputPath, invocationOptions, ct) => + { + return 1; // Simulate failure + }; + + return runner; + }; + }); + + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("new aspire-starter --use-redis-cache --test-framework None"); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + var executionContext = provider.GetRequiredService(); + var expectedMessage = string.Format(CultureInfo.CurrentCulture, TemplatingStrings.ProjectCreationFailed, 1, executionContext.LogFilePath); + + Assert.NotEqual(0, exitCode); + Assert.NotNull(testInteractionService); + Assert.Contains(expectedMessage, testInteractionService.DisplayedErrors); + } + + [Fact] + public async Task NewCommand_WhenTypeScriptTemplateApplyFails_ReturnsNonZeroExitCode() + { + TestInteractionService? testInteractionService = null; + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, options => + { + options.InteractionServiceFactory = (sp) => + { + testInteractionService = new TestInteractionService(); + return testInteractionService; + }; + + options.DotNetCliRunnerFactory = (sp) => + { + var runner = new TestDotNetCliRunner(); + runner.SearchPackagesAsyncCallback = (dir, query, prerelease, take, skip, nugetSource, useCache, options, cancellationToken) => + { + var package = new NuGetPackage() + { + Id = "Aspire.ProjectTemplates", + Source = "nuget", + Version = "9.2.0" + }; + + return (0, new NuGetPackage[] { package }); + }; + + return runner; + }; + }); + + services.AddSingleton(new TestScaffoldingService + { + ScaffoldAsyncCallback = (context, cancellationToken) => + { + return Task.FromResult(false); // Simulate failure for TypeScript template + } + }); + + var provider = services.BuildServiceProvider(); + + var command = provider.GetRequiredService(); + var result = command.Parse("new aspire-ts-empty --name TestApp --output ."); + + var exitCode = await result.InvokeAsync().DefaultTimeout(); + + Assert.NotEqual(0, exitCode); + Assert.NotNull(testInteractionService); + } } internal sealed class TestNewCommandPrompter(IInteractionService interactionService) : NewCommandPrompter(interactionService) diff --git a/tests/Aspire.Cli.Tests/Configuration/AspireConfigFileTests.cs b/tests/Aspire.Cli.Tests/Configuration/AspireConfigFileTests.cs index 4f2fcfee40a..f7556f11b89 100644 --- a/tests/Aspire.Cli.Tests/Configuration/AspireConfigFileTests.cs +++ b/tests/Aspire.Cli.Tests/Configuration/AspireConfigFileTests.cs @@ -306,6 +306,69 @@ public void GetIntegrationReferences_IncludesPackagesAndBasePackage() Assert.Contains(refs, r => r.Name == "Aspire.Hosting.Redis"); } + [Fact] + public void Load_ReturnsConfig_WhenFeaturesAreBooleans() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, AspireConfigFile.FileName); + File.WriteAllText(configPath, """ + { + "features": { "polyglotSupportEnabled": true, "showAllTemplates": false } + } + """); + + var result = AspireConfigFile.Load(workspace.WorkspaceRoot.FullName); + + Assert.NotNull(result); + Assert.NotNull(result.Features); + Assert.True(result.Features["polyglotSupportEnabled"]); + Assert.False(result.Features["showAllTemplates"]); + } + + [Fact] + public void Load_ReturnsConfig_WhenFeaturesAreStringBooleans() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + // Simulates what happens when ConfigurationService.SetNestedValue wrote "true"/"false" as strings + var configPath = Path.Combine(workspace.WorkspaceRoot.FullName, AspireConfigFile.FileName); + File.WriteAllText(configPath, """ + { + "features": { "polyglotSupportEnabled": "true", "showAllTemplates": "false" } + } + """); + + var result = AspireConfigFile.Load(workspace.WorkspaceRoot.FullName); + + Assert.NotNull(result); + Assert.NotNull(result.Features); + Assert.True(result.Features["polyglotSupportEnabled"]); + Assert.False(result.Features["showAllTemplates"]); + } + + [Fact] + public void Save_Load_RoundTrips_WithFeatures() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var config = new AspireConfigFile + { + Features = new Dictionary + { + ["polyglotSupportEnabled"] = true, + ["showAllTemplates"] = false + } + }; + + config.Save(workspace.WorkspaceRoot.FullName); + var loaded = AspireConfigFile.Load(workspace.WorkspaceRoot.FullName); + + Assert.NotNull(loaded?.Features); + Assert.True(loaded.Features["polyglotSupportEnabled"]); + Assert.False(loaded.Features["showAllTemplates"]); + } + [Fact] public void Load_RoundTrips_WithProfiles() { @@ -336,4 +399,274 @@ public void Load_RoundTrips_WithProfiles() Assert.True(loaded.Profiles.ContainsKey("default")); Assert.Equal("https://localhost:5001", loaded.Profiles["default"].ApplicationUrl); } + + [Fact] + public void LoadOrCreate_MigratesLegacy_AdjustsRelativePathFromAspireDir() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + // Legacy .aspire/settings.json stores paths relative to the .aspire/ directory + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, """ + { + "appHostPath": "../src/apphost.ts", + "language": "typescript/nodejs", + "sdkVersion": "13.2.0" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + // Path should be re-based from .aspire/-relative to root-relative + Assert.Equal("src/apphost.ts", config.AppHost?.Path); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_AdjustsPathForApphostAtRoot() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + // Legacy path "../apphost.ts" means apphost is at the repo root + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, """ + { + "appHostPath": "../apphost.ts", + "language": "typescript/nodejs" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + Assert.Equal("apphost.ts", config.AppHost?.Path); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_RebasesSubdirectoryPath() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + // Legacy .aspire/settings.json stores appHostPath relative to .aspire/ directory. + // A path like "../MyApp.AppHost/MyApp.AppHost.csproj" points from .aspire/ up to + // the repo root, then into a subdirectory. After migration it should become + // "MyApp.AppHost/MyApp.AppHost.csproj" (relative to the repo root). + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, """ + { + "appHostPath": "../MyApp.AppHost/MyApp.AppHost.csproj" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + Assert.Equal("MyApp.AppHost/MyApp.AppHost.csproj", config.AppHost?.Path); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_SavesConfigFile() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, """ + { + "appHostPath": "../src/apphost.ts", + "sdkVersion": "13.2.0" + } + """); + + AspireConfigFile.LoadOrCreate(root); + + // Verify aspire.config.json was created with correct content + var configPath = Path.Combine(root, AspireConfigFile.FileName); + Assert.True(File.Exists(configPath)); + + var saved = AspireConfigFile.Load(root); + Assert.NotNull(saved); + Assert.Equal("src/apphost.ts", saved.AppHost?.Path); + Assert.Equal("13.2.0", saved.SdkVersion); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_LeavesAbsolutePathUnchanged() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + var absolutePath = Path.Combine(root, "src", "apphost.ts").Replace(Path.DirectorySeparatorChar, '/'); + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, $$""" + { + "appHostPath": "{{absolutePath}}" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + // Absolute paths should not be modified + Assert.Equal(absolutePath, config.AppHost?.Path); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_NormalizesBackslashSeparators() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + // Simulate a settings file created on Windows with backslash separators. + // Even though we always store '/', handle '\' gracefully in case of manual edits. + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, """ + { + "appHostPath": "..\\src\\apphost.ts", + "language": "typescript/nodejs" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + // Should be re-based and normalized to forward slashes + Assert.Equal("src/apphost.ts", config.AppHost?.Path); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_OutputAlwaysUsesForwardSlashes() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, """ + { + "appHostPath": "../deeply/nested/path/apphost.ts" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + // Verify output uses forward slashes regardless of platform + Assert.Equal("deeply/nested/path/apphost.ts", config.AppHost?.Path); + Assert.DoesNotContain("\\", config.AppHost?.Path); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_SkipsEmptyPath() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, """ + { + "appHostPath": "", + "language": "typescript/nodejs" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + // Empty path should not be transformed to "." + Assert.Equal("", config.AppHost?.Path); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_SkipsNullPath() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, """ + { + "language": "typescript/nodejs" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + // No appHostPath means no migration needed; path stays null + Assert.Null(config.AppHost?.Path); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_DotSlashRelativePath() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + // "./MyApp.AppHost/apphost.ts" from .aspire/ dir resolves to .aspire/MyApp.AppHost/apphost.ts + // relative to root. While unusual, verifies dot-slash handling doesn't break. + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, """ + { + "appHostPath": "./../MyApp.AppHost/apphost.ts" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + Assert.Equal("MyApp.AppHost/apphost.ts", config.AppHost?.Path); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_BareRelativePath() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + // A bare relative path without ../ from .aspire/ stays under .aspire/ when resolved. + // In practice legacy paths always start with ../ but we verify the math is correct. + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, """ + { + "appHostPath": "../MyApp.AppHost/MyApp.AppHost.csproj" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + Assert.Equal("MyApp.AppHost/MyApp.AppHost.csproj", config.AppHost?.Path); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_LeavesUnixRootedPathUnchanged() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, """ + { + "appHostPath": "/path/apphost.ts" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + Assert.Equal("/path/apphost.ts", config.AppHost?.Path); + } + + [Fact] + public void LoadOrCreate_MigratesLegacy_LeavesWindowsRootedPathUnchanged() + { + Assert.SkipUnless(OperatingSystem.IsWindows(), "Windows-rooted paths are only recognized on Windows."); + + using var workspace = TemporaryWorkspace.Create(outputHelper); + var root = workspace.WorkspaceRoot.FullName; + + var settingsPath = Path.Combine(root, ".aspire", "settings.json"); + File.WriteAllText(settingsPath, $$""" + { + "appHostPath": "c:\\path\\apphost.ts" + } + """); + + var config = AspireConfigFile.LoadOrCreate(root); + + // On Windows, c:\ is rooted and should be left unchanged + Assert.Equal("c:\\path\\apphost.ts", config.AppHost?.Path); + } } diff --git a/tests/Aspire.Cli.Tests/Configuration/ConfigurationHelperTests.cs b/tests/Aspire.Cli.Tests/Configuration/ConfigurationHelperTests.cs index cdea30079d2..f65125ca022 100644 --- a/tests/Aspire.Cli.Tests/Configuration/ConfigurationHelperTests.cs +++ b/tests/Aspire.Cli.Tests/Configuration/ConfigurationHelperTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.Json; +using System.Text.Json.Nodes; using Aspire.Cli.Configuration; using Aspire.Cli.Tests.Utils; using Aspire.Cli.Utils; @@ -108,4 +110,35 @@ public void RegisterSettingsFiles_HandlesCommentsAndTrailingCommas() Assert.Equal("MyApp.csproj", config["appHost:path"]); Assert.Equal("daily", config["channel"]); } + + [Fact] + public void TryNormalizeSettingsFile_PreservesBooleanTypes() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var settingsPath = Path.Combine(workspace.WorkspaceRoot.FullName, AspireConfigFile.FileName); + // File has a colon-separated key with a boolean value + File.WriteAllText(settingsPath, """ + { + "features:polyglotSupportEnabled": true, + "features:showAllTemplates": false + } + """); + + var normalized = ConfigurationHelper.TryNormalizeSettingsFile(settingsPath); + + Assert.True(normalized); + + var json = JsonNode.Parse(File.ReadAllText(settingsPath)); + var polyglotNode = json!["features"]!["polyglotSupportEnabled"]; + var templatesNode = json!["features"]!["showAllTemplates"]; + Assert.Equal(JsonValueKind.True, polyglotNode!.GetValueKind()); + Assert.Equal(JsonValueKind.False, templatesNode!.GetValueKind()); + + // Verify the file can be loaded by AspireConfigFile without error + var config = AspireConfigFile.Load(workspace.WorkspaceRoot.FullName); + Assert.NotNull(config?.Features); + Assert.True(config.Features["polyglotSupportEnabled"]); + Assert.False(config.Features["showAllTemplates"]); + } } diff --git a/tests/Aspire.Cli.Tests/Configuration/ConfigurationServiceTests.cs b/tests/Aspire.Cli.Tests/Configuration/ConfigurationServiceTests.cs index 8b10267db2b..f0ff9161d10 100644 --- a/tests/Aspire.Cli.Tests/Configuration/ConfigurationServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Configuration/ConfigurationServiceTests.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System.Text.Json; +using System.Text.Json.Nodes; using Aspire.Cli.Configuration; using Aspire.Cli.Tests.Utils; using Microsoft.Extensions.Configuration; @@ -225,4 +227,64 @@ public async Task SetConfigurationAsync_SetsNestedValues() Assert.Contains("appHost", result); Assert.Contains("MyApp/MyApp.csproj", result); } + + [Fact] + public async Task SetConfigurationAsync_WritesBooleanStringAsJsonString() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var (service, settingsFilePath) = CreateService(workspace, "{}"); + + await service.SetConfigurationAsync("features.polyglotSupportEnabled", "true", isGlobal: false); + + // Value is written as a JSON string "true", not a JSON boolean true. + // The FlexibleBooleanConverter handles parsing "true" -> bool on read. + var json = JsonNode.Parse(File.ReadAllText(settingsFilePath)); + var node = json!["features"]!["polyglotSupportEnabled"]; + Assert.Equal(JsonValueKind.String, node!.GetValueKind()); + Assert.Equal("true", node.GetValue()); + + // Verify round-trip through AspireConfigFile.Load still works + var config = AspireConfigFile.Load(workspace.WorkspaceRoot.FullName); + Assert.NotNull(config?.Features); + Assert.True(config.Features["polyglotSupportEnabled"]); + } + + [Fact] + public async Task SetConfigurationAsync_ChannelWithBooleanLikeValue_StaysAsString() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var (service, settingsFilePath) = CreateService(workspace, "{}"); + + // "true" is a valid channel value and must remain a string in JSON + // to avoid corrupting the string-typed Channel property. + await service.SetConfigurationAsync("channel", "true", isGlobal: false); + + // Must be a JSON string "true", not a JSON boolean true + var json = JsonNode.Parse(File.ReadAllText(settingsFilePath)); + var node = json!["channel"]; + Assert.Equal(JsonValueKind.String, node!.GetValueKind()); + Assert.Equal("true", node.GetValue()); + + // Verify it round-trips correctly through AspireConfigFile.Load + var config = AspireConfigFile.Load(workspace.WorkspaceRoot.FullName); + Assert.NotNull(config); + Assert.Equal("true", config.Channel); + } + + [Fact] + public async Task SetConfigurationAsync_WritesStringValueAsString() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var (service, settingsFilePath) = CreateService(workspace, "{}"); + + await service.SetConfigurationAsync("channel", "daily", isGlobal: false); + + var json = JsonNode.Parse(File.ReadAllText(settingsFilePath)); + var node = json!["channel"]; + Assert.Equal(JsonValueKind.String, node!.GetValueKind()); + Assert.Equal("daily", node.GetValue()); + } } diff --git a/tests/Aspire.Cli.Tests/DotNetSdkInstallerTests.cs b/tests/Aspire.Cli.Tests/DotNetSdkInstallerTests.cs index f7bf937ef07..3a6c5c61af9 100644 --- a/tests/Aspire.Cli.Tests/DotNetSdkInstallerTests.cs +++ b/tests/Aspire.Cli.Tests/DotNetSdkInstallerTests.cs @@ -167,7 +167,7 @@ public void ErrorMessage_Format_IsCorrect() "10.0.100", "(not found)"); - Assert.Equal("The Aspire CLI requires .NET SDK version 10.0.100 or later. Detected: (not found).", message); + Assert.Equal("C# apphost requires .NET SDK version 10.0.100 or later. Detected: (not found).", message); } [Fact] diff --git a/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs b/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs index 34fe7383c2c..846c2ada0be 100644 --- a/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Interaction/ConsoleInteractionServiceTests.cs @@ -123,8 +123,8 @@ public void DisplayLines_WithMarkupCharacters_DoesNotCauseMarkupParsingError() Assert.Null(exception); var outputString = output.ToString(); Assert.Contains("Command output with brackets", outputString); - // Square brackets get escaped to [[square]] when using EscapeMarkup() - Assert.Contains("Error output with [[square]] brackets", outputString); + // EscapeMarkup() escapes [ to [[ for Spectre's parser, but Spectre renders [[ back to literal [ + Assert.Contains("Error output with [square] brackets", outputString); } [Fact] diff --git a/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs b/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs index 51ce56b6907..7cf4720ef09 100644 --- a/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs +++ b/tests/Aspire.Cli.Tests/Packaging/PackagingServiceTests.cs @@ -382,7 +382,8 @@ public async Task NuGetConfigMerger_WhenChannelRequiresGlobalPackagesFolder_Adds var globalPackagesFolderAdd = configSection.Elements("add") .FirstOrDefault(add => string.Equals((string?)add.Attribute("key"), "globalPackagesFolder", StringComparison.OrdinalIgnoreCase)); Assert.NotNull(globalPackagesFolderAdd); - Assert.Equal(".nugetpackages", (string?)globalPackagesFolderAdd.Attribute("value")); + var actualGlobalPackagesFolder = (string?)globalPackagesFolderAdd.Attribute("value"); + Assert.Equal(".nugetpackages", actualGlobalPackagesFolder); } [Fact] diff --git a/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs b/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs index 2deebbb0aca..89ac6a12b2b 100644 --- a/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/AppHostServerProjectTests.cs @@ -2,11 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.AspNetCore.InternalTesting; +using System.Security.Cryptography; +using System.Text; using System.Xml.Linq; using Aspire.Cli.Configuration; using Aspire.Cli.NuGet; using Aspire.Cli.Packaging; using Aspire.Cli.Projects; +using Aspire.Cli.Utils; using Aspire.Cli.Tests.Mcp; using Aspire.Cli.Tests.TestServices; using Aspire.Cli.Tests.Utils; @@ -186,6 +189,38 @@ public void ProjectModelPath_IsStableForSameAppPath() Assert.Equal(project1.ProjectModelPath, project2.ProjectModelPath); } + [Fact] + public void ProjectModelPath_UsesUserAspireDirectory() + { + // Arrange + var project = CreateProject(); + var normalizedAppPath = Path.GetFullPath(project.AppDirectoryPath); + normalizedAppPath = new Uri(normalizedAppPath).LocalPath; + normalizedAppPath = OperatingSystem.IsWindows() ? normalizedAppPath.ToLowerInvariant() : normalizedAppPath; + + var pathHash = SHA256.HashData(Encoding.UTF8.GetBytes(normalizedAppPath)); + var pathDir = Convert.ToHexString(pathHash)[..12].ToLowerInvariant(); + var expectedRoot = Path.Combine(CliPathHelper.GetAspireHomeDirectory(), "hosts"); + var expectedPath = Path.Combine(expectedRoot, pathDir); + var parentDirectory = Path.GetDirectoryName(project.ProjectModelPath); + var isSafeToDelete = string.Equals(project.ProjectModelPath, expectedPath, StringComparison.OrdinalIgnoreCase) && + parentDirectory is not null && + string.Equals(parentDirectory, expectedRoot, StringComparison.OrdinalIgnoreCase); + + try + { + // Assert + Assert.Equal(expectedPath, project.ProjectModelPath); + } + finally + { + if (isSafeToDelete && Directory.Exists(project.ProjectModelPath)) + { + Directory.Delete(project.ProjectModelPath, recursive: true); + } + } + } + [Fact] public void UserSecretsId_IsStableForSameAppPath() { diff --git a/tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs b/tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs new file mode 100644 index 00000000000..de3e08ef937 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Projects/AppHostServerSessionTests.cs @@ -0,0 +1,75 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics; +using Aspire.Cli.Configuration; +using Aspire.Cli.Projects; +using Aspire.Cli.Utils; +using Aspire.Hosting; +using Microsoft.Extensions.Logging.Abstractions; + +namespace Aspire.Cli.Tests.Projects; + +public class AppHostServerSessionTests +{ + [Fact] + public async Task Start_DoesNotMutateCallerEnvironmentVariables() + { + // Arrange + var project = new RecordingAppHostServerProject(); + var environmentVariables = new Dictionary + { + ["EXISTING_VALUE"] = "present" + }; + + // Act + await using var session = AppHostServerSession.Start( + project, + environmentVariables, + debug: false, + NullLogger.Instance); + + // Assert + Assert.Equal("present", environmentVariables["EXISTING_VALUE"]); + Assert.False(environmentVariables.ContainsKey(KnownConfigNames.RemoteAppHostToken)); + + Assert.NotNull(project.ReceivedEnvironmentVariables); + Assert.Equal("present", project.ReceivedEnvironmentVariables["EXISTING_VALUE"]); + Assert.Equal(session.AuthenticationToken, project.ReceivedEnvironmentVariables[KnownConfigNames.RemoteAppHostToken]); + } + + private sealed class RecordingAppHostServerProject : IAppHostServerProject + { + public string AppDirectoryPath => Directory.GetCurrentDirectory(); + + public Dictionary? ReceivedEnvironmentVariables { get; private set; } + + public string GetInstanceIdentifier() => AppDirectoryPath; + + public Task PrepareAsync( + string sdkVersion, + IEnumerable integrations, + CancellationToken cancellationToken = default) => + throw new NotSupportedException(); + + public (string SocketPath, Process Process, OutputCollector OutputCollector) Run( + int hostPid, + IReadOnlyDictionary? environmentVariables = null, + string[]? additionalArgs = null, + bool debug = false) + { + ReceivedEnvironmentVariables = environmentVariables is null + ? null + : new Dictionary(environmentVariables); + + var process = Process.Start(new ProcessStartInfo("dotnet", "--version") + { + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false + })!; + + return ("test.sock", process, new OutputCollector()); + } + } +} diff --git a/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs b/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs index 20f5f6f9765..0caee5d34db 100644 --- a/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs @@ -302,7 +302,7 @@ await Verify(content, extension: "json") [Fact] public void GetServerEnvironmentVariables_ParsesLaunchSettingsWithComments() { - var project = CreateGuestAppHostProject(_workspace.WorkspaceRoot); + var project = CreateGuestAppHostProject(); var propertiesDir = _workspace.CreateDirectory("Properties"); var launchSettingsPath = Path.Combine(propertiesDir.FullName, "launchSettings.json"); @@ -333,7 +333,7 @@ public void GetServerEnvironmentVariables_ParsesLaunchSettingsWithComments() Assert.False(envVars.ContainsKey("ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL")); } - private static GuestAppHostProject CreateGuestAppHostProject(DirectoryInfo workspaceRoot) + private static GuestAppHostProject CreateGuestAppHostProject() { var language = new LanguageInfo( LanguageId: "typescript/nodejs", @@ -342,13 +342,6 @@ private static GuestAppHostProject CreateGuestAppHostProject(DirectoryInfo works DetectionPatterns: ["apphost.ts"], CodeGenerator: "TypeScript"); - // Point the config service at a non-existent file so GetConfigDirectory - // falls back to the directory we pass to GetServerEnvironmentVariables. - var configService = new TestConfigurationService - { - SettingsFilePath = Path.Combine(workspaceRoot.FullName, "nonexistent", "settings.json") - }; - var configuration = new ConfigurationBuilder().Build(); var logFilePath = Path.Combine(Path.GetTempPath(), $"test-guest-{Guid.NewGuid()}.log"); @@ -362,7 +355,6 @@ private static GuestAppHostProject CreateGuestAppHostProject(DirectoryInfo works runner: new TestDotNetCliRunner(), packagingService: new TestPackagingService(), configuration: configuration, - configurationService: configService, features: new Features(configuration, NullLogger.Instance), languageDiscovery: new TestLanguageDiscovery(), logger: NullLogger.Instance, diff --git a/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs b/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs index d659bb978d0..8888eff0583 100644 --- a/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/PrebuiltAppHostServerTests.cs @@ -3,11 +3,16 @@ using System.Xml.Linq; using Aspire.Cli.Configuration; +using Aspire.Cli.Layout; +using Aspire.Cli.NuGet; using Aspire.Cli.Projects; +using Aspire.Cli.Tests.TestServices; +using Aspire.Cli.Tests.Utils; +using Aspire.Cli.Utils; namespace Aspire.Cli.Tests.Projects; -public class PrebuiltAppHostServerTests +public class PrebuiltAppHostServerTests(ITestOutputHelper outputHelper) { [Fact] public void GenerateIntegrationProjectFile_WithPackagesOnly_ProducesPackageReferences() @@ -145,4 +150,46 @@ public void GenerateIntegrationProjectFile_WithEmptyAdditionalSources_DoesNotSet Assert.Null(restoreSources); } + [Fact] + public void Constructor_UsesUserAspireDirectoryForWorkingDirectory() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var nugetService = new BundleNuGetService(new NullLayoutDiscovery(), Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + var server = new PrebuiltAppHostServer( + workspace.WorkspaceRoot.FullName, + "test.sock", + new LayoutConfiguration(), + nugetService, + new TestDotNetCliRunner(), + new TestDotNetSdkInstaller(), + new Aspire.Cli.Tests.Mcp.MockPackagingService(), + new TestConfigurationService(), + Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance); + + var workingDirectory = Assert.IsType( + typeof(PrebuiltAppHostServer) + .GetField("_workingDirectory", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic)! + .GetValue(server)); + + var rootDirectory = Path.Combine(CliPathHelper.GetAspireHomeDirectory(), "bundle-hosts"); + var isUnderRoot = workingDirectory.StartsWith(rootDirectory, StringComparison.OrdinalIgnoreCase); + var parentDirectory = Path.GetDirectoryName(workingDirectory); + var isDirectChildOfRoot = parentDirectory is not null && + string.Equals(parentDirectory, rootDirectory, StringComparison.OrdinalIgnoreCase); + var isSafeToDelete = isUnderRoot && isDirectChildOfRoot && !string.Equals(workingDirectory, rootDirectory, StringComparison.OrdinalIgnoreCase); + + try + { + Assert.True(isSafeToDelete); + } + finally + { + if (isSafeToDelete && Directory.Exists(workingDirectory)) + { + Directory.Delete(workingDirectory, recursive: true); + } + } + } + } diff --git a/tests/Aspire.Cli.Tests/Utils/AppHostHelperTests.cs b/tests/Aspire.Cli.Tests/Utils/AppHostHelperTests.cs index 23f862445ed..87c2fcc5dca 100644 --- a/tests/Aspire.Cli.Tests/Utils/AppHostHelperTests.cs +++ b/tests/Aspire.Cli.Tests/Utils/AppHostHelperTests.cs @@ -97,14 +97,25 @@ public void ComputeAuxiliarySocketPrefix_HashIs16Characters() [Fact] public void ExtractHashFromSocketPath_ExtractsHashFromNewFormat() { - // New format: auxi.sock.{hash}.{pid} - var socketPath = "/home/user/.aspire/cli/backchannels/auxi.sock.abc123def4567890.12345"; + // Current format: auxi.sock.{hash}.{instanceHash}.{pid} + var socketPath = "/home/user/.aspire/cli/backchannels/auxi.sock.abc123def4567890.a1b2c3d4e5f6.12345"; var hash = AppHostHelper.ExtractHashFromSocketPath(socketPath); Assert.Equal("abc123def4567890", hash); } + [Fact] + public void ExtractHashFromSocketPath_ExtractsHashFromPreviousFormat() + { + // Previous format: auxi.sock.{hash}.{pid} + var socketPath = "/home/user/.aspire/cli/backchannels/auxi.sock.abc123def4567890.12345"; + + var hash = AppHostHelper.ExtractHashFromSocketPath(socketPath); + + Assert.Equal("abc123def4567890", hash); + } + [Fact] public void ExtractHashFromSocketPath_ExtractsHashFromOldFormat() { @@ -140,14 +151,25 @@ public void ExtractHashFromSocketPath_ReturnsNullForUnrecognizedFormat() [Fact] public void ExtractPidFromSocketPath_ExtractsPidFromNewFormat() { - // New format: auxi.sock.{hash}.{pid} - var socketPath = "/home/user/.aspire/cli/backchannels/auxi.sock.abc123def4567890.12345"; + // Current format: auxi.sock.{hash}.{instanceHash}.{pid} + var socketPath = "/home/user/.aspire/cli/backchannels/auxi.sock.abc123def4567890.a1b2c3d4e5f6.12345"; var pid = AppHostHelper.ExtractPidFromSocketPath(socketPath); Assert.Equal(12345, pid); } + [Fact] + public void ExtractPidFromSocketPath_ExtractsPidFromPreviousFormat() + { + // Previous format: auxi.sock.{hash}.{pid} + var socketPath = "/home/user/.aspire/cli/backchannels/auxi.sock.abc123def4567890.12345"; + + var pid = AppHostHelper.ExtractPidFromSocketPath(socketPath); + + Assert.Equal(12345, pid); + } + [Fact] public void ExtractPidFromSocketPath_ReturnsNullForOldFormat() { @@ -215,8 +237,8 @@ public void FindMatchingSockets_FindsMatchingSocketFiles() var prefix = AppHostHelper.ComputeAuxiliarySocketPrefix(appHostPath, workspace.WorkspaceRoot.FullName); var hash = Path.GetFileName(prefix)["auxi.sock.".Length..]; - // Create matching socket files (new format with PID) - var socket1 = Path.Combine(backchannelsDir, $"auxi.sock.{hash}.12345"); + // Create matching socket files in both current and previous formats. + var socket1 = Path.Combine(backchannelsDir, $"auxi.sock.{hash}.a1b2c3d4e5f6.12345"); var socket2 = Path.Combine(backchannelsDir, $"auxi.sock.{hash}.67890"); File.WriteAllText(socket1, ""); File.WriteAllText(socket2, ""); diff --git a/tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs b/tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs new file mode 100644 index 00000000000..2c570508e98 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Utils/CliPathHelperTests.cs @@ -0,0 +1,51 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Utils; + +namespace Aspire.Cli.Tests.Utils; + +public class CliPathHelperTests(ITestOutputHelper outputHelper) +{ + [Fact] + public void CreateGuestAppHostSocketPath_UsesRandomizedIdentifier() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var socketPath1 = CliPathHelper.CreateGuestAppHostSocketPath("apphost.sock"); + var socketPath2 = CliPathHelper.CreateGuestAppHostSocketPath("apphost.sock"); + + Assert.NotEqual(socketPath1, socketPath2); + + if (OperatingSystem.IsWindows()) + { + Assert.Matches("^apphost\\.sock\\.[a-f0-9]{12}$", socketPath1); + Assert.Matches("^apphost\\.sock\\.[a-f0-9]{12}$", socketPath2); + } + else + { + var expectedDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".aspire", "cli", "runtime", "sockets"); + Assert.Equal(expectedDirectory, Path.GetDirectoryName(socketPath1)); + Assert.Equal(expectedDirectory, Path.GetDirectoryName(socketPath2)); + Assert.Matches("^apphost\\.sock\\.[a-f0-9]{12}$", Path.GetFileName(socketPath1)); + Assert.Matches("^apphost\\.sock\\.[a-f0-9]{12}$", Path.GetFileName(socketPath2)); + } + } + + [Fact] + public void CreateUnixDomainSocketPath_UsesRandomizedIdentifier() + { + using var workspace = TemporaryWorkspace.Create(outputHelper); + + var socketPath1 = CliPathHelper.CreateUnixDomainSocketPath("apphost.sock"); + var socketPath2 = CliPathHelper.CreateUnixDomainSocketPath("apphost.sock"); + + Assert.NotEqual(socketPath1, socketPath2); + + var expectedDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".aspire", "cli", "runtime", "sockets"); + Assert.Equal(expectedDirectory, Path.GetDirectoryName(socketPath1)); + Assert.Equal(expectedDirectory, Path.GetDirectoryName(socketPath2)); + Assert.Matches("^apphost\\.sock\\.[a-f0-9]{12}$", Path.GetFileName(socketPath1)); + Assert.Matches("^apphost\\.sock\\.[a-f0-9]{12}$", Path.GetFileName(socketPath2)); + } +} diff --git a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs index 02606af4380..8b7b9c97af0 100644 --- a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs +++ b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs @@ -247,11 +247,13 @@ private CliExecutionContext CreateDefaultCliExecutionContextFactory(IServiceProv var cacheDirectory = new DirectoryInfo(Path.Combine(WorkingDirectory.FullName, ".aspire", "cache")); var logsDirectory = new DirectoryInfo(Path.Combine(WorkingDirectory.FullName, ".aspire", "logs")); var logFilePath = Path.Combine(logsDirectory.FullName, "test.log"); - return new CliExecutionContext(WorkingDirectory, hivesDirectory, cacheDirectory, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-sdks")), logsDirectory, logFilePath); + return new CliExecutionContext(WorkingDirectory, hivesDirectory, cacheDirectory, new DirectoryInfo(Path.Combine(Path.GetTempPath(), "aspire-test-sdks")), logsDirectory, logFilePath, packagesDirectory: PackagesDirectory); } public DirectoryInfo WorkingDirectory { get; set; } + public DirectoryInfo? PackagesDirectory { get; set; } + public Action> ConfigurationCallback { get; set; } = (Dictionary config) => { }; diff --git a/tests/Aspire.Dashboard.Tests/Model/AIAssistant/AssistantChatDataContextTests.cs b/tests/Aspire.Dashboard.Tests/Model/AIAssistant/AssistantChatDataContextTests.cs index bf57c341dac..22ef2cf8f1c 100644 --- a/tests/Aspire.Dashboard.Tests/Model/AIAssistant/AssistantChatDataContextTests.cs +++ b/tests/Aspire.Dashboard.Tests/Model/AIAssistant/AssistantChatDataContextTests.cs @@ -47,7 +47,7 @@ public async Task GetStructuredLogs_ExceedTokenLimit_ReturnMostRecentItems() var dataContext = CreateAssistantChatDataContext(telemetryRepository: repository); // Act - var result = await dataContext.GetStructuredLogsAsync(resourceName: null, CancellationToken.None); + var result = await dataContext.GetStructuredLogsAsync(resourceName: null!, CancellationToken.None); // Assert for (var i = 6; i < 20; i++) @@ -81,7 +81,7 @@ public async Task GetTraces_ExceedTokenLimit_ReturnMostRecentItems() var dataContext = CreateAssistantChatDataContext(telemetryRepository: repository); // Act - var result = await dataContext.GetTracesAsync(resourceName: null, CancellationToken.None); + var result = await dataContext.GetTracesAsync(resourceName: null!, CancellationToken.None); // Assert for (var i = 7; i < 20; i++) diff --git a/tests/Aspire.Hosting.Analyzers.Tests/AspireExportAnalyzerTests.cs b/tests/Aspire.Hosting.Analyzers.Tests/AspireExportAnalyzerTests.cs index 7cbdf609038..b46904fcd02 100644 --- a/tests/Aspire.Hosting.Analyzers.Tests/AspireExportAnalyzerTests.cs +++ b/tests/Aspire.Hosting.Analyzers.Tests/AspireExportAnalyzerTests.cs @@ -84,6 +84,25 @@ public class TestExports await test.RunAsync(); } + [Fact] + public async Task InstanceMethodOnExportedType_NoDiagnostics() + { + var test = AnalyzerTest.Create(""" + using Aspire.Hosting; + + var builder = DistributedApplication.CreateBuilder(args); + + [AspireExport] + public class TestExports + { + [AspireExport("instanceMethod")] + public string InstanceMethod() => "test"; + } + """, []); + + await test.RunAsync(); + } + [Fact] public async Task InvalidIdFormat_WithSlash_ReportsASPIREEXPORT002() { @@ -228,6 +247,28 @@ public static class TestExports await test.RunAsync(); } + [Fact] + public async Task ValidValueTaskReturn_NoDiagnostics() + { + var test = AnalyzerTest.Create(""" + using Aspire.Hosting; + using System.Threading.Tasks; + + var builder = DistributedApplication.CreateBuilder(args); + + public static class TestExports + { + [AspireExport("asyncMethod")] + public static ValueTask AsyncMethod() => ValueTask.CompletedTask; + + [AspireExport("asyncMethodWithResult")] + public static ValueTask AsyncMethodWithResult() => ValueTask.FromResult("test"); + } + """, []); + + await test.RunAsync(); + } + [Fact] public async Task ValidEnumTypes_NoDiagnostics() { @@ -675,6 +716,48 @@ public static void Method(MyCustomType custom) { } await test.RunAsync(); } + [Fact] + public async Task AssemblyExportedExternalType_NoDiagnostics() + { + var test = AnalyzerTest.Create(""" + using System; + using Aspire.Hosting; + + [assembly: AspireExport(typeof(IServiceProvider))] + + var builder = DistributedApplication.CreateBuilder(args); + + public static class TestExports + { + [AspireExport("getProvider")] + public static IServiceProvider GetProvider(this IServiceProvider provider) => provider; + } + """, []); + + await test.RunAsync(); + } + + [Fact] + public async Task ArrayOfAssemblyExportedExternalType_NoDiagnostics() + { + var test = AnalyzerTest.Create(""" + using System; + using Aspire.Hosting; + + [assembly: AspireExport(typeof(IServiceProvider))] + + var builder = DistributedApplication.CreateBuilder(args); + + public static class TestExports + { + [AspireExport("getChildren")] + public static IServiceProvider[] GetChildren(IServiceProvider[] providers) => providers; + } + """, []); + + await test.RunAsync(); + } + [Fact] public async Task ValidObjectType_NoDiagnostics() { @@ -1282,6 +1365,24 @@ public static void AddThing(this IDistributedApplicationBuilder builder, string await test.RunAsync(); } + [Fact] + public async Task MissingExportAttribute_WithContainingTypeAspireExportIgnore_NoDiagnostics() + { + var test = AnalyzerTest.Create(""" + using Aspire.Hosting; + + var builder = DistributedApplication.CreateBuilder(args); + + [AspireExportIgnore(Reason = "Not part of the ATS surface.")] + public static class TestExtensions + { + public static void AddThing(this IDistributedApplicationBuilder builder, string name) { } + } + """, []); + + await test.RunAsync(); + } + [Fact] public async Task MissingExportAttribute_ObsoleteMethod_NoDiagnostics() { @@ -1386,6 +1487,29 @@ public static void Configure(this AssemblyExportedHandle handle) { } await test.RunAsync(); } + [Fact] + public async Task MissingExportAttribute_OnAssemblyExportedExternalTypeExtension_ReportsASPIREEXPORT008() + { + var diagnostic = AspireExportAnalyzer.Diagnostics.s_missingExportAttribute; + + var test = AnalyzerTest.Create(""" + using System; + using Aspire.Hosting; + + [assembly: AspireExport(typeof(IServiceProvider))] + + var builder = DistributedApplication.CreateBuilder(args); + + public static class TestExports + { + public static void Configure(this IServiceProvider serviceProvider) { } + } + """, + [new DiagnosticResult(diagnostic).WithLocation(10, 24).WithArguments("Configure", "Add [AspireExport] if ATS-compatible, or [AspireExportIgnore] with a reason.")]); + + await test.RunAsync(); + } + [Fact] public async Task MissingExportAttribute_OnImplicitlyExportedResourceTypeExtension_ReportsASPIREEXPORT008() { @@ -1534,6 +1658,29 @@ internal static IResourceBuilder WithEnvironment( await test.RunAsync(); } + [Fact] + public async Task ExportNameMatchesMethodName_WithInterfaceTarget_NoDiagnostics() + { + var test = AnalyzerTest.Create(""" + using Aspire.Hosting; + using Aspire.Hosting.ApplicationModel; + + var builder = DistributedApplication.CreateBuilder(args); + + public static class TestExports + { + [AspireExport("withReference")] + internal static IResourceBuilder WithReference( + this IResourceBuilder builder, + IResourceBuilder target) + where T : IResourceWithEnvironment + => builder; + } + """, []); + + await test.RunAsync(); + } + [Fact] public async Task ExportNameMatchesMethodName_ConcreteFirstParam_NoDiagnostics() { diff --git a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs index c921cddf0a4..b029174d695 100644 --- a/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs +++ b/tests/Aspire.Hosting.Azure.Kusto.Tests/AddAzureKustoTests.cs @@ -27,7 +27,7 @@ public void AddAzureKustoCluster_ShouldCreateAzureKustoClusterResourceWithCorrec } [Theory] - [InlineData(null, "latest")] + [InlineData(null, "2026.03.16.1116-2611-994a3c9-master")] [InlineData("custom-tag", "custom-tag")] public void RunAsEmulator_ShouldConfigureContainerImageWithCorrectTag(string? customTag, string expectedTag) { diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs index 493b3d25b37..3b6d74fe9ed 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureCosmosDBExtensionsTests.cs @@ -58,7 +58,7 @@ public void AddAzureCosmosDBWithEmulatorGetsExpectedImageTag(string imageTag) Assert.NotNull(containerImageAnnotation); var actualTag = containerImageAnnotation.Tag; - Assert.Equal(imageTag ?? "latest", actualTag); + Assert.Equal(imageTag ?? "stable", actualTag); } [Theory] diff --git a/tests/Aspire.Hosting.Azure.Tests/FoundryExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/FoundryExtensionsTests.cs index 41b2dfdc90f..4729d4cf244 100644 --- a/tests/Aspire.Hosting.Azure.Tests/FoundryExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/FoundryExtensionsTests.cs @@ -194,6 +194,20 @@ public void AddProject_SetsParentFoundryForProvisioningOrdering() Assert.Same(foundry.Resource, project.Resource.Parent); } + [Fact] + public void AddProject_AddsDefaultContainerRegistryInRunMode() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); + + var project = builder.AddFoundry("myAIFoundry") + .AddProject("my-project"); + + var registry = Assert.Single(builder.Resources.OfType()); + + Assert.Equal("my-project-acr", registry.Name); + Assert.Same(registry, project.Resource.ContainerRegistry); + } + [Fact] public async Task AddProject_WithPublishAsExistingFoundry_GeneratesBicepThatReferencesExistingParent() { diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go index a14a280e0bd..b47ff5fc31c 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/AtsGeneratedAspire.verified.go @@ -1395,6 +1395,13 @@ func Connect() (*AspireClient, error) { if err := client.Connect(); err != nil { return nil, err } + authToken := os.Getenv("ASPIRE_REMOTE_APPHOST_TOKEN") + if authToken == "" { + return nil, fmt.Errorf("ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`") + } + if err := client.Authenticate(authToken); err != nil { + return nil, err + } client.OnDisconnect(func() { os.Exit(1) }) return client, nil } diff --git a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go index 7a9f7be1652..215bc231ff3 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go +++ b/tests/Aspire.Hosting.CodeGeneration.Go.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.go @@ -568,34 +568,6 @@ func (s *CSharpAppResource) WithRequiredCommand(command string, helpLink *string return result.(*IResource), nil } -// WithEnvironment sets an environment variable -func (s *CSharpAppResource) WithEnvironment(name string, value string) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentExpression adds an environment variable with a reference expression -func (s *CSharpAppResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *CSharpAppResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -611,29 +583,28 @@ func (s *CSharpAppResource) WithEnvironmentCallback(callback func(...any) any) ( return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentCallbackAsync sets environment variables via async callback -func (s *CSharpAppResource) WithEnvironmentCallbackAsync(callback func(...any) any) (*IResourceWithEnvironment, error) { +// WithEnvironmentEndpoint sets an environment variable from an endpoint reference +func (s *CSharpAppResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } - if callback != nil { - reqArgs["callback"] = RegisterCallback(callback) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs) + reqArgs["name"] = SerializeValue(name) + reqArgs["endpointReference"] = SerializeValue(endpointReference) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) if err != nil { return nil, err } return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *CSharpAppResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { +// WithEnvironment sets an environment variable on the resource +func (s *CSharpAppResource) WithEnvironment(name string, value any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) + reqArgs["value"] = SerializeValue(value) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) if err != nil { return nil, err } @@ -1001,7 +972,7 @@ func (s *CSharpAppResource) PublishWithContainerFiles(source *IResourceWithConta } reqArgs["source"] = SerializeValue(source) reqArgs["destinationPath"] = SerializeValue(destinationPath) - result, err := s.Client().InvokeCapability("Aspire.Hosting/publishWithContainerFiles", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/publishWithContainerFilesFromResource", reqArgs) if err != nil { return nil, err } @@ -1026,7 +997,7 @@ func (s *CSharpAppResource) WaitFor(dependency *IResource) (*IResourceWithWaitSu "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitFor", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResource", reqArgs) if err != nil { return nil, err } @@ -1053,7 +1024,7 @@ func (s *CSharpAppResource) WaitForStart(dependency *IResource) (*IResourceWithW "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForStart", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs) if err != nil { return nil, err } @@ -1095,7 +1066,7 @@ func (s *CSharpAppResource) WaitForCompletion(dependency *IResource, exitCode *f if exitCode != nil { reqArgs["exitCode"] = SerializeValue(exitCode) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForCompletion", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs) if err != nil { return nil, err } @@ -1190,7 +1161,7 @@ func (s *CSharpAppResource) WithHttpsDeveloperCertificate(password *ParameterRes if password != nil { reqArgs["password"] = SerializeValue(password) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs) if err != nil { return nil, err } @@ -1215,7 +1186,7 @@ func (s *CSharpAppResource) WithParentRelationship(parent *IResource) (*IResourc "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -1228,7 +1199,7 @@ func (s *CSharpAppResource) WithChildRelationship(child *IResource) (*IResource, "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -1894,6 +1865,19 @@ func (s *ConnectionStringResource) WithConnectionPropertyValue(name string, valu return result.(*IResourceWithConnectionString), nil } +// GetConnectionProperty gets a connection property by key +func (s *ConnectionStringResource) GetConnectionProperty(key string) (*ReferenceExpression, error) { + reqArgs := map[string]any{ + "resource": SerializeValue(s.Handle()), + } + reqArgs["key"] = SerializeValue(key) + result, err := s.Client().InvokeCapability("Aspire.Hosting/getConnectionProperty", reqArgs) + if err != nil { + return nil, err + } + return result.(*ReferenceExpression), nil +} + // WithUrlsCallback customizes displayed URLs via callback func (s *ConnectionStringResource) WithUrlsCallback(callback func(...any) any) (*IResource, error) { reqArgs := map[string]any{ @@ -1990,7 +1974,7 @@ func (s *ConnectionStringResource) WaitFor(dependency *IResource) (*IResourceWit "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitFor", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResource", reqArgs) if err != nil { return nil, err } @@ -2017,7 +2001,7 @@ func (s *ConnectionStringResource) WaitForStart(dependency *IResource) (*IResour "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForStart", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs) if err != nil { return nil, err } @@ -2059,7 +2043,7 @@ func (s *ConnectionStringResource) WaitForCompletion(dependency *IResource, exit if exitCode != nil { reqArgs["exitCode"] = SerializeValue(exitCode) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForCompletion", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs) if err != nil { return nil, err } @@ -2105,7 +2089,7 @@ func (s *ConnectionStringResource) WithParentRelationship(parent *IResource) (*I "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -2118,7 +2102,7 @@ func (s *ConnectionStringResource) WithChildRelationship(child *IResource) (*IRe "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -2704,7 +2688,7 @@ func (s *ContainerRegistryResource) WithParentRelationship(parent *IResource) (* "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -2717,7 +2701,7 @@ func (s *ContainerRegistryResource) WithChildRelationship(child *IResource) (*IR "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -3262,7 +3246,7 @@ func (s *ContainerResource) WithBuildArg(name string, value *ParameterResource) } reqArgs["name"] = SerializeValue(name) reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuildArg", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterBuildArg", reqArgs) if err != nil { return nil, err } @@ -3276,7 +3260,7 @@ func (s *ContainerResource) WithBuildSecret(name string, value *ParameterResourc } reqArgs["name"] = SerializeValue(name) reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuildSecret", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterBuildSecret", reqArgs) if err != nil { return nil, err } @@ -3398,34 +3382,6 @@ func (s *ContainerResource) WithRequiredCommand(command string, helpLink *string return result.(*IResource), nil } -// WithEnvironment sets an environment variable -func (s *ContainerResource) WithEnvironment(name string, value string) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentExpression adds an environment variable with a reference expression -func (s *ContainerResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *ContainerResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -3441,29 +3397,28 @@ func (s *ContainerResource) WithEnvironmentCallback(callback func(...any) any) ( return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentCallbackAsync sets environment variables via async callback -func (s *ContainerResource) WithEnvironmentCallbackAsync(callback func(...any) any) (*IResourceWithEnvironment, error) { +// WithEnvironmentEndpoint sets an environment variable from an endpoint reference +func (s *ContainerResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } - if callback != nil { - reqArgs["callback"] = RegisterCallback(callback) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs) + reqArgs["name"] = SerializeValue(name) + reqArgs["endpointReference"] = SerializeValue(endpointReference) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) if err != nil { return nil, err } return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *ContainerResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { +// WithEnvironment sets an environment variable on the resource +func (s *ContainerResource) WithEnvironment(name string, value any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) + reqArgs["value"] = SerializeValue(value) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) if err != nil { return nil, err } @@ -3842,7 +3797,7 @@ func (s *ContainerResource) WaitFor(dependency *IResource) (*IResourceWithWaitSu "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitFor", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResource", reqArgs) if err != nil { return nil, err } @@ -3869,7 +3824,7 @@ func (s *ContainerResource) WaitForStart(dependency *IResource) (*IResourceWithW "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForStart", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs) if err != nil { return nil, err } @@ -3911,7 +3866,7 @@ func (s *ContainerResource) WaitForCompletion(dependency *IResource, exitCode *f if exitCode != nil { reqArgs["exitCode"] = SerializeValue(exitCode) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForCompletion", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs) if err != nil { return nil, err } @@ -4006,7 +3961,7 @@ func (s *ContainerResource) WithHttpsDeveloperCertificate(password *ParameterRes if password != nil { reqArgs["password"] = SerializeValue(password) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs) if err != nil { return nil, err } @@ -4031,7 +3986,7 @@ func (s *ContainerResource) WithParentRelationship(parent *IResource) (*IResourc "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -4044,7 +3999,7 @@ func (s *ContainerResource) WithChildRelationship(child *IResource) (*IResource, "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -4923,34 +4878,6 @@ func (s *DotnetToolResource) WithRequiredCommand(command string, helpLink *strin return result.(*IResource), nil } -// WithEnvironment sets an environment variable -func (s *DotnetToolResource) WithEnvironment(name string, value string) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentExpression adds an environment variable with a reference expression -func (s *DotnetToolResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *DotnetToolResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -4966,29 +4893,28 @@ func (s *DotnetToolResource) WithEnvironmentCallback(callback func(...any) any) return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentCallbackAsync sets environment variables via async callback -func (s *DotnetToolResource) WithEnvironmentCallbackAsync(callback func(...any) any) (*IResourceWithEnvironment, error) { +// WithEnvironmentEndpoint sets an environment variable from an endpoint reference +func (s *DotnetToolResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } - if callback != nil { - reqArgs["callback"] = RegisterCallback(callback) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs) + reqArgs["name"] = SerializeValue(name) + reqArgs["endpointReference"] = SerializeValue(endpointReference) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) if err != nil { return nil, err } return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *DotnetToolResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { +// WithEnvironment sets an environment variable on the resource +func (s *DotnetToolResource) WithEnvironment(name string, value any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) + reqArgs["value"] = SerializeValue(value) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) if err != nil { return nil, err } @@ -5367,7 +5293,7 @@ func (s *DotnetToolResource) WaitFor(dependency *IResource) (*IResourceWithWaitS "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitFor", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResource", reqArgs) if err != nil { return nil, err } @@ -5394,7 +5320,7 @@ func (s *DotnetToolResource) WaitForStart(dependency *IResource) (*IResourceWith "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForStart", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs) if err != nil { return nil, err } @@ -5436,7 +5362,7 @@ func (s *DotnetToolResource) WaitForCompletion(dependency *IResource, exitCode * if exitCode != nil { reqArgs["exitCode"] = SerializeValue(exitCode) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForCompletion", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs) if err != nil { return nil, err } @@ -5531,7 +5457,7 @@ func (s *DotnetToolResource) WithHttpsDeveloperCertificate(password *ParameterRe if password != nil { reqArgs["password"] = SerializeValue(password) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs) if err != nil { return nil, err } @@ -5556,7 +5482,7 @@ func (s *DotnetToolResource) WithParentRelationship(parent *IResource) (*IResour "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -5569,7 +5495,7 @@ func (s *DotnetToolResource) WithChildRelationship(child *IResource) (*IResource "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -6512,34 +6438,6 @@ func (s *ExecutableResource) WithRequiredCommand(command string, helpLink *strin return result.(*IResource), nil } -// WithEnvironment sets an environment variable -func (s *ExecutableResource) WithEnvironment(name string, value string) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentExpression adds an environment variable with a reference expression -func (s *ExecutableResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *ExecutableResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -6555,29 +6453,28 @@ func (s *ExecutableResource) WithEnvironmentCallback(callback func(...any) any) return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentCallbackAsync sets environment variables via async callback -func (s *ExecutableResource) WithEnvironmentCallbackAsync(callback func(...any) any) (*IResourceWithEnvironment, error) { +// WithEnvironmentEndpoint sets an environment variable from an endpoint reference +func (s *ExecutableResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } - if callback != nil { - reqArgs["callback"] = RegisterCallback(callback) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs) + reqArgs["name"] = SerializeValue(name) + reqArgs["endpointReference"] = SerializeValue(endpointReference) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) if err != nil { return nil, err } return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *ExecutableResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { +// WithEnvironment sets an environment variable on the resource +func (s *ExecutableResource) WithEnvironment(name string, value any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) + reqArgs["value"] = SerializeValue(value) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) if err != nil { return nil, err } @@ -6956,7 +6853,7 @@ func (s *ExecutableResource) WaitFor(dependency *IResource) (*IResourceWithWaitS "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitFor", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResource", reqArgs) if err != nil { return nil, err } @@ -6983,7 +6880,7 @@ func (s *ExecutableResource) WaitForStart(dependency *IResource) (*IResourceWith "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForStart", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs) if err != nil { return nil, err } @@ -7025,7 +6922,7 @@ func (s *ExecutableResource) WaitForCompletion(dependency *IResource, exitCode * if exitCode != nil { reqArgs["exitCode"] = SerializeValue(exitCode) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForCompletion", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs) if err != nil { return nil, err } @@ -7120,7 +7017,7 @@ func (s *ExecutableResource) WithHttpsDeveloperCertificate(password *ParameterRe if password != nil { reqArgs["password"] = SerializeValue(password) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs) if err != nil { return nil, err } @@ -7145,7 +7042,7 @@ func (s *ExecutableResource) WithParentRelationship(parent *IResource) (*IResour "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -7158,7 +7055,7 @@ func (s *ExecutableResource) WithChildRelationship(child *IResource) (*IResource "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -7913,7 +7810,7 @@ func (s *ExternalServiceResource) WithParentRelationship(parent *IResource) (*IR "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -7926,7 +7823,7 @@ func (s *ExternalServiceResource) WithChildRelationship(child *IResource) (*IRes "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -9785,7 +9682,7 @@ func (s *ParameterResource) WithParentRelationship(parent *IResource) (*IResourc "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -9798,7 +9695,7 @@ func (s *ParameterResource) WithChildRelationship(child *IResource) (*IResource, "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -10887,34 +10784,6 @@ func (s *ProjectResource) WithRequiredCommand(command string, helpLink *string) return result.(*IResource), nil } -// WithEnvironment sets an environment variable -func (s *ProjectResource) WithEnvironment(name string, value string) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentExpression adds an environment variable with a reference expression -func (s *ProjectResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *ProjectResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -10930,29 +10799,28 @@ func (s *ProjectResource) WithEnvironmentCallback(callback func(...any) any) (*I return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentCallbackAsync sets environment variables via async callback -func (s *ProjectResource) WithEnvironmentCallbackAsync(callback func(...any) any) (*IResourceWithEnvironment, error) { +// WithEnvironmentEndpoint sets an environment variable from an endpoint reference +func (s *ProjectResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } - if callback != nil { - reqArgs["callback"] = RegisterCallback(callback) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs) + reqArgs["name"] = SerializeValue(name) + reqArgs["endpointReference"] = SerializeValue(endpointReference) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) if err != nil { return nil, err } return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *ProjectResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { +// WithEnvironment sets an environment variable on the resource +func (s *ProjectResource) WithEnvironment(name string, value any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) + reqArgs["value"] = SerializeValue(value) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) if err != nil { return nil, err } @@ -11320,7 +11188,7 @@ func (s *ProjectResource) PublishWithContainerFiles(source *IResourceWithContain } reqArgs["source"] = SerializeValue(source) reqArgs["destinationPath"] = SerializeValue(destinationPath) - result, err := s.Client().InvokeCapability("Aspire.Hosting/publishWithContainerFiles", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/publishWithContainerFilesFromResource", reqArgs) if err != nil { return nil, err } @@ -11345,7 +11213,7 @@ func (s *ProjectResource) WaitFor(dependency *IResource) (*IResourceWithWaitSupp "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitFor", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResource", reqArgs) if err != nil { return nil, err } @@ -11372,7 +11240,7 @@ func (s *ProjectResource) WaitForStart(dependency *IResource) (*IResourceWithWai "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForStart", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs) if err != nil { return nil, err } @@ -11414,7 +11282,7 @@ func (s *ProjectResource) WaitForCompletion(dependency *IResource, exitCode *flo if exitCode != nil { reqArgs["exitCode"] = SerializeValue(exitCode) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForCompletion", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs) if err != nil { return nil, err } @@ -11509,7 +11377,7 @@ func (s *ProjectResource) WithHttpsDeveloperCertificate(password *ParameterResou if password != nil { reqArgs["password"] = SerializeValue(password) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs) if err != nil { return nil, err } @@ -11534,7 +11402,7 @@ func (s *ProjectResource) WithParentRelationship(parent *IResource) (*IResource, "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -11547,7 +11415,7 @@ func (s *ProjectResource) WithChildRelationship(child *IResource) (*IResource, e "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -12786,7 +12654,7 @@ func (s *TestDatabaseResource) WithBuildArg(name string, value *ParameterResourc } reqArgs["name"] = SerializeValue(name) reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuildArg", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterBuildArg", reqArgs) if err != nil { return nil, err } @@ -12800,7 +12668,7 @@ func (s *TestDatabaseResource) WithBuildSecret(name string, value *ParameterReso } reqArgs["name"] = SerializeValue(name) reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuildSecret", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterBuildSecret", reqArgs) if err != nil { return nil, err } @@ -12922,34 +12790,6 @@ func (s *TestDatabaseResource) WithRequiredCommand(command string, helpLink *str return result.(*IResource), nil } -// WithEnvironment sets an environment variable -func (s *TestDatabaseResource) WithEnvironment(name string, value string) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentExpression adds an environment variable with a reference expression -func (s *TestDatabaseResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *TestDatabaseResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -12965,29 +12805,28 @@ func (s *TestDatabaseResource) WithEnvironmentCallback(callback func(...any) any return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentCallbackAsync sets environment variables via async callback -func (s *TestDatabaseResource) WithEnvironmentCallbackAsync(callback func(...any) any) (*IResourceWithEnvironment, error) { +// WithEnvironmentEndpoint sets an environment variable from an endpoint reference +func (s *TestDatabaseResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } - if callback != nil { - reqArgs["callback"] = RegisterCallback(callback) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs) + reqArgs["name"] = SerializeValue(name) + reqArgs["endpointReference"] = SerializeValue(endpointReference) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) if err != nil { return nil, err } return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *TestDatabaseResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { +// WithEnvironment sets an environment variable on the resource +func (s *TestDatabaseResource) WithEnvironment(name string, value any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) + reqArgs["value"] = SerializeValue(value) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) if err != nil { return nil, err } @@ -13366,7 +13205,7 @@ func (s *TestDatabaseResource) WaitFor(dependency *IResource) (*IResourceWithWai "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitFor", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResource", reqArgs) if err != nil { return nil, err } @@ -13393,7 +13232,7 @@ func (s *TestDatabaseResource) WaitForStart(dependency *IResource) (*IResourceWi "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForStart", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs) if err != nil { return nil, err } @@ -13435,7 +13274,7 @@ func (s *TestDatabaseResource) WaitForCompletion(dependency *IResource, exitCode if exitCode != nil { reqArgs["exitCode"] = SerializeValue(exitCode) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForCompletion", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs) if err != nil { return nil, err } @@ -13530,7 +13369,7 @@ func (s *TestDatabaseResource) WithHttpsDeveloperCertificate(password *Parameter if password != nil { reqArgs["password"] = SerializeValue(password) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs) if err != nil { return nil, err } @@ -13555,7 +13394,7 @@ func (s *TestDatabaseResource) WithParentRelationship(parent *IResource) (*IReso "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -13568,7 +13407,7 @@ func (s *TestDatabaseResource) WithChildRelationship(child *IResource) (*IResour "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -14324,7 +14163,7 @@ func (s *TestRedisResource) WithBuildArg(name string, value *ParameterResource) } reqArgs["name"] = SerializeValue(name) reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuildArg", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterBuildArg", reqArgs) if err != nil { return nil, err } @@ -14338,7 +14177,7 @@ func (s *TestRedisResource) WithBuildSecret(name string, value *ParameterResourc } reqArgs["name"] = SerializeValue(name) reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuildSecret", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterBuildSecret", reqArgs) if err != nil { return nil, err } @@ -14460,34 +14299,6 @@ func (s *TestRedisResource) WithRequiredCommand(command string, helpLink *string return result.(*IResource), nil } -// WithEnvironment sets an environment variable -func (s *TestRedisResource) WithEnvironment(name string, value string) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentExpression adds an environment variable with a reference expression -func (s *TestRedisResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *TestRedisResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -14503,29 +14314,28 @@ func (s *TestRedisResource) WithEnvironmentCallback(callback func(...any) any) ( return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentCallbackAsync sets environment variables via async callback -func (s *TestRedisResource) WithEnvironmentCallbackAsync(callback func(...any) any) (*IResourceWithEnvironment, error) { +// WithEnvironmentEndpoint sets an environment variable from an endpoint reference +func (s *TestRedisResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } - if callback != nil { - reqArgs["callback"] = RegisterCallback(callback) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs) + reqArgs["name"] = SerializeValue(name) + reqArgs["endpointReference"] = SerializeValue(endpointReference) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) if err != nil { return nil, err } return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *TestRedisResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { +// WithEnvironment sets an environment variable on the resource +func (s *TestRedisResource) WithEnvironment(name string, value any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) + reqArgs["value"] = SerializeValue(value) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) if err != nil { return nil, err } @@ -14653,6 +14463,19 @@ func (s *TestRedisResource) WithReference(source *IResource, connectionName *str return result.(*IResourceWithEnvironment), nil } +// GetConnectionProperty gets a connection property by key +func (s *TestRedisResource) GetConnectionProperty(key string) (*ReferenceExpression, error) { + reqArgs := map[string]any{ + "resource": SerializeValue(s.Handle()), + } + reqArgs["key"] = SerializeValue(key) + result, err := s.Client().InvokeCapability("Aspire.Hosting/getConnectionProperty", reqArgs) + if err != nil { + return nil, err + } + return result.(*ReferenceExpression), nil +} + // WithReferenceUri adds a reference to a URI func (s *TestRedisResource) WithReferenceUri(name string, uri string) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -14932,7 +14755,7 @@ func (s *TestRedisResource) WaitFor(dependency *IResource) (*IResourceWithWaitSu "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitFor", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResource", reqArgs) if err != nil { return nil, err } @@ -14959,7 +14782,7 @@ func (s *TestRedisResource) WaitForStart(dependency *IResource) (*IResourceWithW "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForStart", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs) if err != nil { return nil, err } @@ -15001,7 +14824,7 @@ func (s *TestRedisResource) WaitForCompletion(dependency *IResource, exitCode *f if exitCode != nil { reqArgs["exitCode"] = SerializeValue(exitCode) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForCompletion", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs) if err != nil { return nil, err } @@ -15096,7 +14919,7 @@ func (s *TestRedisResource) WithHttpsDeveloperCertificate(password *ParameterRes if password != nil { reqArgs["password"] = SerializeValue(password) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs) if err != nil { return nil, err } @@ -15121,7 +14944,7 @@ func (s *TestRedisResource) WithParentRelationship(parent *IResource) (*IResourc "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -15134,7 +14957,7 @@ func (s *TestRedisResource) WithChildRelationship(child *IResource) (*IResource, "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -16074,7 +15897,7 @@ func (s *TestVaultResource) WithBuildArg(name string, value *ParameterResource) } reqArgs["name"] = SerializeValue(name) reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuildArg", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterBuildArg", reqArgs) if err != nil { return nil, err } @@ -16088,7 +15911,7 @@ func (s *TestVaultResource) WithBuildSecret(name string, value *ParameterResourc } reqArgs["name"] = SerializeValue(name) reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuildSecret", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterBuildSecret", reqArgs) if err != nil { return nil, err } @@ -16210,34 +16033,6 @@ func (s *TestVaultResource) WithRequiredCommand(command string, helpLink *string return result.(*IResource), nil } -// WithEnvironment sets an environment variable -func (s *TestVaultResource) WithEnvironment(name string, value string) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - -// WithEnvironmentExpression adds an environment variable with a reference expression -func (s *TestVaultResource) WithEnvironmentExpression(name string, value *ReferenceExpression) (*IResourceWithEnvironment, error) { - reqArgs := map[string]any{ - "builder": SerializeValue(s.Handle()), - } - reqArgs["name"] = SerializeValue(name) - reqArgs["value"] = SerializeValue(value) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs) - if err != nil { - return nil, err - } - return result.(*IResourceWithEnvironment), nil -} - // WithEnvironmentCallback sets environment variables via callback func (s *TestVaultResource) WithEnvironmentCallback(callback func(...any) any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ @@ -16253,29 +16048,28 @@ func (s *TestVaultResource) WithEnvironmentCallback(callback func(...any) any) ( return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentCallbackAsync sets environment variables via async callback -func (s *TestVaultResource) WithEnvironmentCallbackAsync(callback func(...any) any) (*IResourceWithEnvironment, error) { +// WithEnvironmentEndpoint sets an environment variable from an endpoint reference +func (s *TestVaultResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } - if callback != nil { - reqArgs["callback"] = RegisterCallback(callback) - } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs) + reqArgs["name"] = SerializeValue(name) + reqArgs["endpointReference"] = SerializeValue(endpointReference) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) if err != nil { return nil, err } return result.(*IResourceWithEnvironment), nil } -// WithEnvironmentEndpoint sets an environment variable from an endpoint reference -func (s *TestVaultResource) WithEnvironmentEndpoint(name string, endpointReference *EndpointReference) (*IResourceWithEnvironment, error) { +// WithEnvironment sets an environment variable on the resource +func (s *TestVaultResource) WithEnvironment(name string, value any) (*IResourceWithEnvironment, error) { reqArgs := map[string]any{ "builder": SerializeValue(s.Handle()), } reqArgs["name"] = SerializeValue(name) - reqArgs["endpointReference"] = SerializeValue(endpointReference) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs) + reqArgs["value"] = SerializeValue(value) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withEnvironment", reqArgs) if err != nil { return nil, err } @@ -16654,7 +16448,7 @@ func (s *TestVaultResource) WaitFor(dependency *IResource) (*IResourceWithWaitSu "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitFor", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResource", reqArgs) if err != nil { return nil, err } @@ -16681,7 +16475,7 @@ func (s *TestVaultResource) WaitForStart(dependency *IResource) (*IResourceWithW "builder": SerializeValue(s.Handle()), } reqArgs["dependency"] = SerializeValue(dependency) - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForStart", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs) if err != nil { return nil, err } @@ -16723,7 +16517,7 @@ func (s *TestVaultResource) WaitForCompletion(dependency *IResource, exitCode *f if exitCode != nil { reqArgs["exitCode"] = SerializeValue(exitCode) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForCompletion", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs) if err != nil { return nil, err } @@ -16818,7 +16612,7 @@ func (s *TestVaultResource) WithHttpsDeveloperCertificate(password *ParameterRes if password != nil { reqArgs["password"] = SerializeValue(password) } - result, err := s.Client().InvokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs) if err != nil { return nil, err } @@ -16843,7 +16637,7 @@ func (s *TestVaultResource) WithParentRelationship(parent *IResource) (*IResourc "builder": SerializeValue(s.Handle()), } reqArgs["parent"] = SerializeValue(parent) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withParentRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs) if err != nil { return nil, err } @@ -16856,7 +16650,7 @@ func (s *TestVaultResource) WithChildRelationship(child *IResource) (*IResource, "builder": SerializeValue(s.Handle()), } reqArgs["child"] = SerializeValue(child) - result, err := s.Client().InvokeCapability("Aspire.Hosting/withChildRelationship", reqArgs) + result, err := s.Client().InvokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs) if err != nil { return nil, err } @@ -17625,6 +17419,13 @@ func Connect() (*AspireClient, error) { if err := client.Connect(); err != nil { return nil, err } + authToken := os.Getenv("ASPIRE_REMOTE_APPHOST_TOKEN") + if authToken == "" { + return nil, fmt.Errorf("ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`") + } + if err := client.Authenticate(authToken); err != nil { + return nil, err + } client.OnDisconnect(func() { os.Exit(1) }) return client, nil } diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/AtsJavaCodeGeneratorTests.cs b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/AtsJavaCodeGeneratorTests.cs index 6011fd89647..2deedf55f87 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/AtsJavaCodeGeneratorTests.cs +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/AtsJavaCodeGeneratorTests.cs @@ -35,6 +35,7 @@ public async Task GenerateDistributedApplication_WithTestTypes_GeneratesCorrectO Assert.Contains("Aspire.java", files.Keys); Assert.Contains("Transport.java", files.Keys); Assert.Contains("Base.java", files.Keys); + Assert.Contains("List params = List.of(token);", files["Transport.java"]); await Verify(files["Aspire.java"], extension: "java") .UseFileName("AtsGeneratedAspire"); diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/AtsGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/AtsGeneratedAspire.verified.java index b24fe84b254..d53cc53ceff 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/AtsGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/AtsGeneratedAspire.verified.java @@ -984,6 +984,11 @@ public static AspireClient connect() throws Exception { } AspireClient client = new AspireClient(socketPath); client.connect(); + String authToken = System.getenv("ASPIRE_REMOTE_APPHOST_TOKEN"); + if (authToken == null || authToken.isEmpty()) { + throw new RuntimeException("ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`."); + } + client.authenticate(authToken); client.onDisconnect(() -> System.exit(1)); return client; } diff --git a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java index daa2e42b8d4..37325d58db8 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java +++ b/tests/Aspire.Hosting.CodeGeneration.Java.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.java @@ -718,24 +718,6 @@ public IResource withRequiredCommand(String command, String helpLink) { return (IResource) getClient().invokeCapability("Aspire.Hosting/withRequiredCommand", reqArgs); } - /** Sets an environment variable */ - public IResourceWithEnvironment withEnvironment(String name, String value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); - } - - /** Adds an environment variable with a reference expression */ - public IResourceWithEnvironment withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - } - /** Sets environment variables via callback */ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { Map reqArgs = new HashMap<>(); @@ -746,16 +728,6 @@ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (callback != null) { - reqArgs.put("callback", getClient().registerCallback(callback)); - } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs); - } - /** Sets an environment variable from an endpoint reference */ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointReference endpointReference) { Map reqArgs = new HashMap<>(); @@ -765,6 +737,15 @@ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointRef return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); } + /** Sets an environment variable on the resource */ + public IResourceWithEnvironment withEnvironment(String name, Object value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("value", AspireClient.serializeValue(value)); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); + } + /** Sets an environment variable from a parameter resource */ public IResourceWithEnvironment withEnvironmentParameter(String name, ParameterResource parameter) { Map reqArgs = new HashMap<>(); @@ -1020,7 +1001,7 @@ public IContainerFilesDestinationResource publishWithContainerFiles(IResourceWit reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("source", AspireClient.serializeValue(source)); reqArgs.put("destinationPath", AspireClient.serializeValue(destinationPath)); - return (IContainerFilesDestinationResource) getClient().invokeCapability("Aspire.Hosting/publishWithContainerFiles", reqArgs); + return (IContainerFilesDestinationResource) getClient().invokeCapability("Aspire.Hosting/publishWithContainerFilesFromResource", reqArgs); } /** Excludes the resource from the deployment manifest */ @@ -1035,7 +1016,7 @@ public IResourceWithWaitSupport waitFor(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitFor", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResource", reqArgs); } /** Waits for another resource with specific behavior */ @@ -1052,7 +1033,7 @@ public IResourceWithWaitSupport waitForStart(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForStart", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs); } /** Waits for another resource to start with specific behavior */ @@ -1079,7 +1060,7 @@ public IResourceWithWaitSupport waitForCompletion(IResource dependency, Double e if (exitCode != null) { reqArgs.put("exitCode", AspireClient.serializeValue(exitCode)); } - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForCompletion", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs); } /** Adds a health check by key */ @@ -1144,7 +1125,7 @@ public IResourceWithEnvironment withHttpsDeveloperCertificate(ParameterResource if (password != null) { reqArgs.put("password", AspireClient.serializeValue(password)); } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs); } /** Removes HTTPS certificate configuration */ @@ -1159,7 +1140,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -1167,7 +1148,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -1607,6 +1588,14 @@ public IResourceWithConnectionString withConnectionPropertyValue(String name, St return (IResourceWithConnectionString) getClient().invokeCapability("Aspire.Hosting/withConnectionPropertyValue", reqArgs); } + /** Gets a connection property by key */ + public ReferenceExpression getConnectionProperty(String key) { + Map reqArgs = new HashMap<>(); + reqArgs.put("resource", AspireClient.serializeValue(getHandle())); + reqArgs.put("key", AspireClient.serializeValue(key)); + return (ReferenceExpression) getClient().invokeCapability("Aspire.Hosting/getConnectionProperty", reqArgs); + } + /** Customizes displayed URLs via callback */ public IResource withUrlsCallback(Function callback) { Map reqArgs = new HashMap<>(); @@ -1672,7 +1661,7 @@ public IResourceWithWaitSupport waitFor(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitFor", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResource", reqArgs); } /** Waits for another resource with specific behavior */ @@ -1689,7 +1678,7 @@ public IResourceWithWaitSupport waitForStart(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForStart", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs); } /** Waits for another resource to start with specific behavior */ @@ -1716,7 +1705,7 @@ public IResourceWithWaitSupport waitForCompletion(IResource dependency, Double e if (exitCode != null) { reqArgs.put("exitCode", AspireClient.serializeValue(exitCode)); } - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForCompletion", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs); } /** Adds a health check by key */ @@ -1747,7 +1736,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -1755,7 +1744,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -2142,7 +2131,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -2150,7 +2139,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -2506,7 +2495,7 @@ public ContainerResource withBuildArg(String name, ParameterResource value) { reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("name", AspireClient.serializeValue(name)); reqArgs.put("value", AspireClient.serializeValue(value)); - return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withBuildArg", reqArgs); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withParameterBuildArg", reqArgs); } /** Adds a build secret from a parameter resource */ @@ -2515,7 +2504,7 @@ public ContainerResource withBuildSecret(String name, ParameterResource value) { reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("name", AspireClient.serializeValue(name)); reqArgs.put("value", AspireClient.serializeValue(value)); - return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withBuildSecret", reqArgs); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withParameterBuildSecret", reqArgs); } /** Configures endpoint proxy support */ @@ -2593,24 +2582,6 @@ public IResource withRequiredCommand(String command, String helpLink) { return (IResource) getClient().invokeCapability("Aspire.Hosting/withRequiredCommand", reqArgs); } - /** Sets an environment variable */ - public IResourceWithEnvironment withEnvironment(String name, String value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); - } - - /** Adds an environment variable with a reference expression */ - public IResourceWithEnvironment withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - } - /** Sets environment variables via callback */ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { Map reqArgs = new HashMap<>(); @@ -2621,16 +2592,6 @@ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (callback != null) { - reqArgs.put("callback", getClient().registerCallback(callback)); - } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs); - } - /** Sets an environment variable from an endpoint reference */ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointReference endpointReference) { Map reqArgs = new HashMap<>(); @@ -2640,6 +2601,15 @@ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointRef return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); } + /** Sets an environment variable on the resource */ + public IResourceWithEnvironment withEnvironment(String name, Object value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("value", AspireClient.serializeValue(value)); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); + } + /** Sets an environment variable from a parameter resource */ public IResourceWithEnvironment withEnvironmentParameter(String name, ParameterResource parameter) { Map reqArgs = new HashMap<>(); @@ -2901,7 +2871,7 @@ public IResourceWithWaitSupport waitFor(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitFor", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResource", reqArgs); } /** Waits for another resource with specific behavior */ @@ -2918,7 +2888,7 @@ public IResourceWithWaitSupport waitForStart(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForStart", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs); } /** Waits for another resource to start with specific behavior */ @@ -2945,7 +2915,7 @@ public IResourceWithWaitSupport waitForCompletion(IResource dependency, Double e if (exitCode != null) { reqArgs.put("exitCode", AspireClient.serializeValue(exitCode)); } - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForCompletion", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs); } /** Adds a health check by key */ @@ -3010,7 +2980,7 @@ public IResourceWithEnvironment withHttpsDeveloperCertificate(ParameterResource if (password != null) { reqArgs.put("password", AspireClient.serializeValue(password)); } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs); } /** Removes HTTPS certificate configuration */ @@ -3025,7 +2995,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -3033,7 +3003,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -3608,24 +3578,6 @@ public IResource withRequiredCommand(String command, String helpLink) { return (IResource) getClient().invokeCapability("Aspire.Hosting/withRequiredCommand", reqArgs); } - /** Sets an environment variable */ - public IResourceWithEnvironment withEnvironment(String name, String value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); - } - - /** Adds an environment variable with a reference expression */ - public IResourceWithEnvironment withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - } - /** Sets environment variables via callback */ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { Map reqArgs = new HashMap<>(); @@ -3636,16 +3588,6 @@ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (callback != null) { - reqArgs.put("callback", getClient().registerCallback(callback)); - } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs); - } - /** Sets an environment variable from an endpoint reference */ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointReference endpointReference) { Map reqArgs = new HashMap<>(); @@ -3655,6 +3597,15 @@ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointRef return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); } + /** Sets an environment variable on the resource */ + public IResourceWithEnvironment withEnvironment(String name, Object value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("value", AspireClient.serializeValue(value)); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); + } + /** Sets an environment variable from a parameter resource */ public IResourceWithEnvironment withEnvironmentParameter(String name, ParameterResource parameter) { Map reqArgs = new HashMap<>(); @@ -3916,7 +3867,7 @@ public IResourceWithWaitSupport waitFor(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitFor", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResource", reqArgs); } /** Waits for another resource with specific behavior */ @@ -3933,7 +3884,7 @@ public IResourceWithWaitSupport waitForStart(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForStart", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs); } /** Waits for another resource to start with specific behavior */ @@ -3960,7 +3911,7 @@ public IResourceWithWaitSupport waitForCompletion(IResource dependency, Double e if (exitCode != null) { reqArgs.put("exitCode", AspireClient.serializeValue(exitCode)); } - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForCompletion", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs); } /** Adds a health check by key */ @@ -4025,7 +3976,7 @@ public IResourceWithEnvironment withHttpsDeveloperCertificate(ParameterResource if (password != null) { reqArgs.put("password", AspireClient.serializeValue(password)); } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs); } /** Removes HTTPS certificate configuration */ @@ -4040,7 +3991,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -4048,7 +3999,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -4656,24 +4607,6 @@ public IResource withRequiredCommand(String command, String helpLink) { return (IResource) getClient().invokeCapability("Aspire.Hosting/withRequiredCommand", reqArgs); } - /** Sets an environment variable */ - public IResourceWithEnvironment withEnvironment(String name, String value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); - } - - /** Adds an environment variable with a reference expression */ - public IResourceWithEnvironment withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - } - /** Sets environment variables via callback */ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { Map reqArgs = new HashMap<>(); @@ -4684,16 +4617,6 @@ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (callback != null) { - reqArgs.put("callback", getClient().registerCallback(callback)); - } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs); - } - /** Sets an environment variable from an endpoint reference */ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointReference endpointReference) { Map reqArgs = new HashMap<>(); @@ -4703,6 +4626,15 @@ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointRef return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); } + /** Sets an environment variable on the resource */ + public IResourceWithEnvironment withEnvironment(String name, Object value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("value", AspireClient.serializeValue(value)); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); + } + /** Sets an environment variable from a parameter resource */ public IResourceWithEnvironment withEnvironmentParameter(String name, ParameterResource parameter) { Map reqArgs = new HashMap<>(); @@ -4964,7 +4896,7 @@ public IResourceWithWaitSupport waitFor(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitFor", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResource", reqArgs); } /** Waits for another resource with specific behavior */ @@ -4981,7 +4913,7 @@ public IResourceWithWaitSupport waitForStart(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForStart", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs); } /** Waits for another resource to start with specific behavior */ @@ -5008,7 +4940,7 @@ public IResourceWithWaitSupport waitForCompletion(IResource dependency, Double e if (exitCode != null) { reqArgs.put("exitCode", AspireClient.serializeValue(exitCode)); } - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForCompletion", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs); } /** Adds a health check by key */ @@ -5073,7 +5005,7 @@ public IResourceWithEnvironment withHttpsDeveloperCertificate(ParameterResource if (password != null) { reqArgs.put("password", AspireClient.serializeValue(password)); } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs); } /** Removes HTTPS certificate configuration */ @@ -5088,7 +5020,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -5096,7 +5028,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -5598,7 +5530,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -5606,7 +5538,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -6854,7 +6786,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -6862,7 +6794,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -7566,24 +7498,6 @@ public IResource withRequiredCommand(String command, String helpLink) { return (IResource) getClient().invokeCapability("Aspire.Hosting/withRequiredCommand", reqArgs); } - /** Sets an environment variable */ - public IResourceWithEnvironment withEnvironment(String name, String value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); - } - - /** Adds an environment variable with a reference expression */ - public IResourceWithEnvironment withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - } - /** Sets environment variables via callback */ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { Map reqArgs = new HashMap<>(); @@ -7594,16 +7508,6 @@ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (callback != null) { - reqArgs.put("callback", getClient().registerCallback(callback)); - } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs); - } - /** Sets an environment variable from an endpoint reference */ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointReference endpointReference) { Map reqArgs = new HashMap<>(); @@ -7613,6 +7517,15 @@ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointRef return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); } + /** Sets an environment variable on the resource */ + public IResourceWithEnvironment withEnvironment(String name, Object value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("value", AspireClient.serializeValue(value)); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); + } + /** Sets an environment variable from a parameter resource */ public IResourceWithEnvironment withEnvironmentParameter(String name, ParameterResource parameter) { Map reqArgs = new HashMap<>(); @@ -7868,7 +7781,7 @@ public IContainerFilesDestinationResource publishWithContainerFiles(IResourceWit reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("source", AspireClient.serializeValue(source)); reqArgs.put("destinationPath", AspireClient.serializeValue(destinationPath)); - return (IContainerFilesDestinationResource) getClient().invokeCapability("Aspire.Hosting/publishWithContainerFiles", reqArgs); + return (IContainerFilesDestinationResource) getClient().invokeCapability("Aspire.Hosting/publishWithContainerFilesFromResource", reqArgs); } /** Excludes the resource from the deployment manifest */ @@ -7883,7 +7796,7 @@ public IResourceWithWaitSupport waitFor(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitFor", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResource", reqArgs); } /** Waits for another resource with specific behavior */ @@ -7900,7 +7813,7 @@ public IResourceWithWaitSupport waitForStart(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForStart", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs); } /** Waits for another resource to start with specific behavior */ @@ -7927,7 +7840,7 @@ public IResourceWithWaitSupport waitForCompletion(IResource dependency, Double e if (exitCode != null) { reqArgs.put("exitCode", AspireClient.serializeValue(exitCode)); } - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForCompletion", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs); } /** Adds a health check by key */ @@ -7992,7 +7905,7 @@ public IResourceWithEnvironment withHttpsDeveloperCertificate(ParameterResource if (password != null) { reqArgs.put("password", AspireClient.serializeValue(password)); } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs); } /** Removes HTTPS certificate configuration */ @@ -8007,7 +7920,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -8015,7 +7928,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -8839,7 +8752,7 @@ public ContainerResource withBuildArg(String name, ParameterResource value) { reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("name", AspireClient.serializeValue(name)); reqArgs.put("value", AspireClient.serializeValue(value)); - return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withBuildArg", reqArgs); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withParameterBuildArg", reqArgs); } /** Adds a build secret from a parameter resource */ @@ -8848,7 +8761,7 @@ public ContainerResource withBuildSecret(String name, ParameterResource value) { reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("name", AspireClient.serializeValue(name)); reqArgs.put("value", AspireClient.serializeValue(value)); - return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withBuildSecret", reqArgs); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withParameterBuildSecret", reqArgs); } /** Configures endpoint proxy support */ @@ -8926,24 +8839,6 @@ public IResource withRequiredCommand(String command, String helpLink) { return (IResource) getClient().invokeCapability("Aspire.Hosting/withRequiredCommand", reqArgs); } - /** Sets an environment variable */ - public IResourceWithEnvironment withEnvironment(String name, String value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); - } - - /** Adds an environment variable with a reference expression */ - public IResourceWithEnvironment withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - } - /** Sets environment variables via callback */ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { Map reqArgs = new HashMap<>(); @@ -8954,16 +8849,6 @@ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (callback != null) { - reqArgs.put("callback", getClient().registerCallback(callback)); - } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs); - } - /** Sets an environment variable from an endpoint reference */ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointReference endpointReference) { Map reqArgs = new HashMap<>(); @@ -8973,6 +8858,15 @@ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointRef return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); } + /** Sets an environment variable on the resource */ + public IResourceWithEnvironment withEnvironment(String name, Object value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("value", AspireClient.serializeValue(value)); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); + } + /** Sets an environment variable from a parameter resource */ public IResourceWithEnvironment withEnvironmentParameter(String name, ParameterResource parameter) { Map reqArgs = new HashMap<>(); @@ -9234,7 +9128,7 @@ public IResourceWithWaitSupport waitFor(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitFor", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResource", reqArgs); } /** Waits for another resource with specific behavior */ @@ -9251,7 +9145,7 @@ public IResourceWithWaitSupport waitForStart(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForStart", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs); } /** Waits for another resource to start with specific behavior */ @@ -9278,7 +9172,7 @@ public IResourceWithWaitSupport waitForCompletion(IResource dependency, Double e if (exitCode != null) { reqArgs.put("exitCode", AspireClient.serializeValue(exitCode)); } - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForCompletion", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs); } /** Adds a health check by key */ @@ -9343,7 +9237,7 @@ public IResourceWithEnvironment withHttpsDeveloperCertificate(ParameterResource if (password != null) { reqArgs.put("password", AspireClient.serializeValue(password)); } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs); } /** Removes HTTPS certificate configuration */ @@ -9358,7 +9252,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -9366,7 +9260,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -9862,7 +9756,7 @@ public ContainerResource withBuildArg(String name, ParameterResource value) { reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("name", AspireClient.serializeValue(name)); reqArgs.put("value", AspireClient.serializeValue(value)); - return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withBuildArg", reqArgs); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withParameterBuildArg", reqArgs); } /** Adds a build secret from a parameter resource */ @@ -9871,7 +9765,7 @@ public ContainerResource withBuildSecret(String name, ParameterResource value) { reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("name", AspireClient.serializeValue(name)); reqArgs.put("value", AspireClient.serializeValue(value)); - return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withBuildSecret", reqArgs); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withParameterBuildSecret", reqArgs); } /** Configures endpoint proxy support */ @@ -9949,24 +9843,6 @@ public IResource withRequiredCommand(String command, String helpLink) { return (IResource) getClient().invokeCapability("Aspire.Hosting/withRequiredCommand", reqArgs); } - /** Sets an environment variable */ - public IResourceWithEnvironment withEnvironment(String name, String value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); - } - - /** Adds an environment variable with a reference expression */ - public IResourceWithEnvironment withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - } - /** Sets environment variables via callback */ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { Map reqArgs = new HashMap<>(); @@ -9977,16 +9853,6 @@ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (callback != null) { - reqArgs.put("callback", getClient().registerCallback(callback)); - } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs); - } - /** Sets an environment variable from an endpoint reference */ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointReference endpointReference) { Map reqArgs = new HashMap<>(); @@ -9996,6 +9862,15 @@ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointRef return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); } + /** Sets an environment variable on the resource */ + public IResourceWithEnvironment withEnvironment(String name, Object value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("value", AspireClient.serializeValue(value)); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); + } + /** Sets an environment variable from a parameter resource */ public IResourceWithEnvironment withEnvironmentParameter(String name, ParameterResource parameter) { Map reqArgs = new HashMap<>(); @@ -10077,6 +9952,14 @@ public IResourceWithEnvironment withReference(IResource source, String connectio return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withReference", reqArgs); } + /** Gets a connection property by key */ + public ReferenceExpression getConnectionProperty(String key) { + Map reqArgs = new HashMap<>(); + reqArgs.put("resource", AspireClient.serializeValue(getHandle())); + reqArgs.put("key", AspireClient.serializeValue(key)); + return (ReferenceExpression) getClient().invokeCapability("Aspire.Hosting/getConnectionProperty", reqArgs); + } + /** Adds a reference to a URI */ public IResourceWithEnvironment withReferenceUri(String name, String uri) { Map reqArgs = new HashMap<>(); @@ -10275,7 +10158,7 @@ public IResourceWithWaitSupport waitFor(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitFor", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResource", reqArgs); } /** Waits for another resource with specific behavior */ @@ -10292,7 +10175,7 @@ public IResourceWithWaitSupport waitForStart(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForStart", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs); } /** Waits for another resource to start with specific behavior */ @@ -10319,7 +10202,7 @@ public IResourceWithWaitSupport waitForCompletion(IResource dependency, Double e if (exitCode != null) { reqArgs.put("exitCode", AspireClient.serializeValue(exitCode)); } - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForCompletion", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs); } /** Adds a health check by key */ @@ -10384,7 +10267,7 @@ public IResourceWithEnvironment withHttpsDeveloperCertificate(ParameterResource if (password != null) { reqArgs.put("password", AspireClient.serializeValue(password)); } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs); } /** Removes HTTPS certificate configuration */ @@ -10399,7 +10282,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -10407,7 +10290,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -11034,7 +10917,7 @@ public ContainerResource withBuildArg(String name, ParameterResource value) { reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("name", AspireClient.serializeValue(name)); reqArgs.put("value", AspireClient.serializeValue(value)); - return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withBuildArg", reqArgs); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withParameterBuildArg", reqArgs); } /** Adds a build secret from a parameter resource */ @@ -11043,7 +10926,7 @@ public ContainerResource withBuildSecret(String name, ParameterResource value) { reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("name", AspireClient.serializeValue(name)); reqArgs.put("value", AspireClient.serializeValue(value)); - return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withBuildSecret", reqArgs); + return (ContainerResource) getClient().invokeCapability("Aspire.Hosting/withParameterBuildSecret", reqArgs); } /** Configures endpoint proxy support */ @@ -11121,24 +11004,6 @@ public IResource withRequiredCommand(String command, String helpLink) { return (IResource) getClient().invokeCapability("Aspire.Hosting/withRequiredCommand", reqArgs); } - /** Sets an environment variable */ - public IResourceWithEnvironment withEnvironment(String name, String value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); - } - - /** Adds an environment variable with a reference expression */ - public IResourceWithEnvironment withEnvironmentExpression(String name, ReferenceExpression value) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - reqArgs.put("name", AspireClient.serializeValue(name)); - reqArgs.put("value", AspireClient.serializeValue(value)); - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentExpression", reqArgs); - } - /** Sets environment variables via callback */ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { Map reqArgs = new HashMap<>(); @@ -11149,16 +11014,6 @@ public IResourceWithEnvironment withEnvironmentCallback(Function callback) { - Map reqArgs = new HashMap<>(); - reqArgs.put("builder", AspireClient.serializeValue(getHandle())); - if (callback != null) { - reqArgs.put("callback", getClient().registerCallback(callback)); - } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentCallbackAsync", reqArgs); - } - /** Sets an environment variable from an endpoint reference */ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointReference endpointReference) { Map reqArgs = new HashMap<>(); @@ -11168,6 +11023,15 @@ public IResourceWithEnvironment withEnvironmentEndpoint(String name, EndpointRef return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironmentEndpoint", reqArgs); } + /** Sets an environment variable on the resource */ + public IResourceWithEnvironment withEnvironment(String name, Object value) { + Map reqArgs = new HashMap<>(); + reqArgs.put("builder", AspireClient.serializeValue(getHandle())); + reqArgs.put("name", AspireClient.serializeValue(name)); + reqArgs.put("value", AspireClient.serializeValue(value)); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withEnvironment", reqArgs); + } + /** Sets an environment variable from a parameter resource */ public IResourceWithEnvironment withEnvironmentParameter(String name, ParameterResource parameter) { Map reqArgs = new HashMap<>(); @@ -11429,7 +11293,7 @@ public IResourceWithWaitSupport waitFor(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitFor", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResource", reqArgs); } /** Waits for another resource with specific behavior */ @@ -11446,7 +11310,7 @@ public IResourceWithWaitSupport waitForStart(IResource dependency) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("dependency", AspireClient.serializeValue(dependency)); - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForStart", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceStart", reqArgs); } /** Waits for another resource to start with specific behavior */ @@ -11473,7 +11337,7 @@ public IResourceWithWaitSupport waitForCompletion(IResource dependency, Double e if (exitCode != null) { reqArgs.put("exitCode", AspireClient.serializeValue(exitCode)); } - return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForCompletion", reqArgs); + return (IResourceWithWaitSupport) getClient().invokeCapability("Aspire.Hosting/waitForResourceCompletion", reqArgs); } /** Adds a health check by key */ @@ -11538,7 +11402,7 @@ public IResourceWithEnvironment withHttpsDeveloperCertificate(ParameterResource if (password != null) { reqArgs.put("password", AspireClient.serializeValue(password)); } - return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withHttpsDeveloperCertificate", reqArgs); + return (IResourceWithEnvironment) getClient().invokeCapability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", reqArgs); } /** Removes HTTPS certificate configuration */ @@ -11553,7 +11417,7 @@ public IResource withParentRelationship(IResource parent) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("parent", AspireClient.serializeValue(parent)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withParentRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderParentRelationship", reqArgs); } /** Sets a child relationship */ @@ -11561,7 +11425,7 @@ public IResource withChildRelationship(IResource child) { Map reqArgs = new HashMap<>(); reqArgs.put("builder", AspireClient.serializeValue(getHandle())); reqArgs.put("child", AspireClient.serializeValue(child)); - return (IResource) getClient().invokeCapability("Aspire.Hosting/withChildRelationship", reqArgs); + return (IResource) getClient().invokeCapability("Aspire.Hosting/withBuilderChildRelationship", reqArgs); } /** Sets the icon for the resource */ @@ -12013,6 +11877,11 @@ public static AspireClient connect() throws Exception { } AspireClient client = new AspireClient(socketPath); client.connect(); + String authToken = System.getenv("ASPIRE_REMOTE_APPHOST_TOKEN"); + if (authToken == null || authToken.isEmpty()) { + throw new RuntimeException("ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`."); + } + client.authenticate(authToken); client.onDisconnect(() -> System.exit(1)); return client; } diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/AtsGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/AtsGeneratedAspire.verified.py index d84dd4677a6..88bd0794508 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/AtsGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/AtsGeneratedAspire.verified.py @@ -724,6 +724,10 @@ def connect() -> AspireClient: raise RuntimeError("REMOTE_APP_HOST_SOCKET_PATH environment variable not set. Run this application using `aspire run`.") client = AspireClient(socket_path) client.connect() + auth_token = os.environ.get("ASPIRE_REMOTE_APPHOST_TOKEN") + if not auth_token: + raise RuntimeError("ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`.") + client.authenticate(auth_token) client.on_disconnect(lambda: sys.exit(1)) return client diff --git a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py index 050a517d447..1ed8d96fecd 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py +++ b/tests/Aspire.Hosting.CodeGeneration.Python.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.py @@ -356,20 +356,6 @@ def with_required_command(self, command: str, help_link: str | None = None) -> I args["helpLink"] = serialize_value(help_link) return self._client.invoke_capability("Aspire.Hosting/withRequiredCommand", args) - def with_environment(self, name: str, value: str) -> IResourceWithEnvironment: - """Sets an environment variable""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) - - def with_environment_expression(self, name: str, value: ReferenceExpression) -> IResourceWithEnvironment: - """Adds an environment variable with a reference expression""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args) - def with_environment_callback(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: """Sets environment variables via callback""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -378,14 +364,6 @@ def with_environment_callback(self, callback: Callable[[EnvironmentCallbackConte args["callback"] = callback_id return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallback", args) - def with_environment_callback_async(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: - """Sets environment variables via async callback""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - callback_id = register_callback(callback) if callback is not None else None - if callback_id is not None: - args["callback"] = callback_id - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args) - def with_environment_endpoint(self, name: str, endpoint_reference: EndpointReference) -> IResourceWithEnvironment: """Sets an environment variable from an endpoint reference""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -393,6 +371,13 @@ def with_environment_endpoint(self, name: str, endpoint_reference: EndpointRefer args["endpointReference"] = serialize_value(endpoint_reference) return self._client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args) + def with_environment(self, name: str, value: str | ReferenceExpression | EndpointReference | ParameterResource | IResourceWithConnectionString) -> IResourceWithEnvironment: + """Sets an environment variable on the resource""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["name"] = serialize_value(name) + args["value"] = serialize_value(value) + return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) + def with_environment_parameter(self, name: str, parameter: ParameterResource) -> IResourceWithEnvironment: """Sets an environment variable from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -578,7 +563,7 @@ def publish_with_container_files(self, source: IResourceWithContainerFiles, dest args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["source"] = serialize_value(source) args["destinationPath"] = serialize_value(destination_path) - return self._client.invoke_capability("Aspire.Hosting/publishWithContainerFiles", args) + return self._client.invoke_capability("Aspire.Hosting/publishWithContainerFilesFromResource", args) def exclude_from_manifest(self) -> IResource: """Excludes the resource from the deployment manifest""" @@ -589,7 +574,7 @@ def wait_for(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to be ready""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitFor", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResource", args) def wait_for_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource with specific behavior""" @@ -602,7 +587,7 @@ def wait_for_start(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to start""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitForStart", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceStart", args) def wait_for_start_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource to start with specific behavior""" @@ -621,7 +606,7 @@ def wait_for_completion(self, dependency: IResource, exit_code: float = 0) -> IR args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) args["exitCode"] = serialize_value(exit_code) - return self._client.invoke_capability("Aspire.Hosting/waitForCompletion", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args) def with_health_check(self, key: str) -> IResource: """Adds a health check by key""" @@ -669,7 +654,7 @@ def with_https_developer_certificate(self, password: ParameterResource | None = args: Dict[str, Any] = { "builder": serialize_value(self._handle) } if password is not None: args["password"] = serialize_value(password) - return self._client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args) def without_https_certificate(self) -> IResourceWithEnvironment: """Removes HTTPS certificate configuration""" @@ -680,13 +665,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -1021,6 +1006,12 @@ def with_connection_property_value(self, name: str, value: str) -> IResourceWith args["value"] = serialize_value(value) return self._client.invoke_capability("Aspire.Hosting/withConnectionPropertyValue", args) + def get_connection_property(self, key: str) -> ReferenceExpression: + """Gets a connection property by key""" + args: Dict[str, Any] = { "resource": serialize_value(self._handle) } + args["key"] = serialize_value(key) + return self._client.invoke_capability("Aspire.Hosting/getConnectionProperty", args) + def with_urls_callback(self, callback: Callable[[ResourceUrlsCallbackContext], None]) -> IResource: """Customizes displayed URLs via callback""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -1071,7 +1062,7 @@ def wait_for(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to be ready""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitFor", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResource", args) def wait_for_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource with specific behavior""" @@ -1084,7 +1075,7 @@ def wait_for_start(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to start""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitForStart", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceStart", args) def wait_for_start_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource to start with specific behavior""" @@ -1103,7 +1094,7 @@ def wait_for_completion(self, dependency: IResource, exit_code: float = 0) -> IR args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) args["exitCode"] = serialize_value(exit_code) - return self._client.invoke_capability("Aspire.Hosting/waitForCompletion", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args) def with_health_check(self, key: str) -> IResource: """Adds a health check by key""" @@ -1127,13 +1118,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -1424,13 +1415,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -1695,14 +1686,14 @@ def with_build_arg(self, name: str, value: ParameterResource) -> ContainerResour args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["name"] = serialize_value(name) args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withBuildArg", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterBuildArg", args) def with_build_secret(self, name: str, value: ParameterResource) -> ContainerResource: """Adds a build secret from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["name"] = serialize_value(name) args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withBuildSecret", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterBuildSecret", args) def with_endpoint_proxy_support(self, proxy_enabled: bool) -> ContainerResource: """Configures endpoint proxy support""" @@ -1757,20 +1748,6 @@ def with_required_command(self, command: str, help_link: str | None = None) -> I args["helpLink"] = serialize_value(help_link) return self._client.invoke_capability("Aspire.Hosting/withRequiredCommand", args) - def with_environment(self, name: str, value: str) -> IResourceWithEnvironment: - """Sets an environment variable""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) - - def with_environment_expression(self, name: str, value: ReferenceExpression) -> IResourceWithEnvironment: - """Adds an environment variable with a reference expression""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args) - def with_environment_callback(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: """Sets environment variables via callback""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -1779,14 +1756,6 @@ def with_environment_callback(self, callback: Callable[[EnvironmentCallbackConte args["callback"] = callback_id return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallback", args) - def with_environment_callback_async(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: - """Sets environment variables via async callback""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - callback_id = register_callback(callback) if callback is not None else None - if callback_id is not None: - args["callback"] = callback_id - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args) - def with_environment_endpoint(self, name: str, endpoint_reference: EndpointReference) -> IResourceWithEnvironment: """Sets an environment variable from an endpoint reference""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -1794,6 +1763,13 @@ def with_environment_endpoint(self, name: str, endpoint_reference: EndpointRefer args["endpointReference"] = serialize_value(endpoint_reference) return self._client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args) + def with_environment(self, name: str, value: str | ReferenceExpression | EndpointReference | ParameterResource | IResourceWithConnectionString) -> IResourceWithEnvironment: + """Sets an environment variable on the resource""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["name"] = serialize_value(name) + args["value"] = serialize_value(value) + return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) + def with_environment_parameter(self, name: str, parameter: ParameterResource) -> IResourceWithEnvironment: """Sets an environment variable from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -1983,7 +1959,7 @@ def wait_for(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to be ready""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitFor", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResource", args) def wait_for_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource with specific behavior""" @@ -1996,7 +1972,7 @@ def wait_for_start(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to start""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitForStart", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceStart", args) def wait_for_start_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource to start with specific behavior""" @@ -2015,7 +1991,7 @@ def wait_for_completion(self, dependency: IResource, exit_code: float = 0) -> IR args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) args["exitCode"] = serialize_value(exit_code) - return self._client.invoke_capability("Aspire.Hosting/waitForCompletion", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args) def with_health_check(self, key: str) -> IResource: """Adds a health check by key""" @@ -2063,7 +2039,7 @@ def with_https_developer_certificate(self, password: ParameterResource | None = args: Dict[str, Any] = { "builder": serialize_value(self._handle) } if password is not None: args["password"] = serialize_value(password) - return self._client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args) def without_https_certificate(self) -> IResourceWithEnvironment: """Removes HTTPS certificate configuration""" @@ -2074,13 +2050,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -2502,20 +2478,6 @@ def with_required_command(self, command: str, help_link: str | None = None) -> I args["helpLink"] = serialize_value(help_link) return self._client.invoke_capability("Aspire.Hosting/withRequiredCommand", args) - def with_environment(self, name: str, value: str) -> IResourceWithEnvironment: - """Sets an environment variable""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) - - def with_environment_expression(self, name: str, value: ReferenceExpression) -> IResourceWithEnvironment: - """Adds an environment variable with a reference expression""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args) - def with_environment_callback(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: """Sets environment variables via callback""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -2524,14 +2486,6 @@ def with_environment_callback(self, callback: Callable[[EnvironmentCallbackConte args["callback"] = callback_id return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallback", args) - def with_environment_callback_async(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: - """Sets environment variables via async callback""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - callback_id = register_callback(callback) if callback is not None else None - if callback_id is not None: - args["callback"] = callback_id - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args) - def with_environment_endpoint(self, name: str, endpoint_reference: EndpointReference) -> IResourceWithEnvironment: """Sets an environment variable from an endpoint reference""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -2539,6 +2493,13 @@ def with_environment_endpoint(self, name: str, endpoint_reference: EndpointRefer args["endpointReference"] = serialize_value(endpoint_reference) return self._client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args) + def with_environment(self, name: str, value: str | ReferenceExpression | EndpointReference | ParameterResource | IResourceWithConnectionString) -> IResourceWithEnvironment: + """Sets an environment variable on the resource""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["name"] = serialize_value(name) + args["value"] = serialize_value(value) + return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) + def with_environment_parameter(self, name: str, parameter: ParameterResource) -> IResourceWithEnvironment: """Sets an environment variable from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -2728,7 +2689,7 @@ def wait_for(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to be ready""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitFor", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResource", args) def wait_for_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource with specific behavior""" @@ -2741,7 +2702,7 @@ def wait_for_start(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to start""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitForStart", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceStart", args) def wait_for_start_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource to start with specific behavior""" @@ -2760,7 +2721,7 @@ def wait_for_completion(self, dependency: IResource, exit_code: float = 0) -> IR args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) args["exitCode"] = serialize_value(exit_code) - return self._client.invoke_capability("Aspire.Hosting/waitForCompletion", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args) def with_health_check(self, key: str) -> IResource: """Adds a health check by key""" @@ -2808,7 +2769,7 @@ def with_https_developer_certificate(self, password: ParameterResource | None = args: Dict[str, Any] = { "builder": serialize_value(self._handle) } if password is not None: args["password"] = serialize_value(password) - return self._client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args) def without_https_certificate(self) -> IResourceWithEnvironment: """Removes HTTPS certificate configuration""" @@ -2819,13 +2780,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -3274,20 +3235,6 @@ def with_required_command(self, command: str, help_link: str | None = None) -> I args["helpLink"] = serialize_value(help_link) return self._client.invoke_capability("Aspire.Hosting/withRequiredCommand", args) - def with_environment(self, name: str, value: str) -> IResourceWithEnvironment: - """Sets an environment variable""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) - - def with_environment_expression(self, name: str, value: ReferenceExpression) -> IResourceWithEnvironment: - """Adds an environment variable with a reference expression""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args) - def with_environment_callback(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: """Sets environment variables via callback""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -3296,14 +3243,6 @@ def with_environment_callback(self, callback: Callable[[EnvironmentCallbackConte args["callback"] = callback_id return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallback", args) - def with_environment_callback_async(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: - """Sets environment variables via async callback""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - callback_id = register_callback(callback) if callback is not None else None - if callback_id is not None: - args["callback"] = callback_id - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args) - def with_environment_endpoint(self, name: str, endpoint_reference: EndpointReference) -> IResourceWithEnvironment: """Sets an environment variable from an endpoint reference""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -3311,6 +3250,13 @@ def with_environment_endpoint(self, name: str, endpoint_reference: EndpointRefer args["endpointReference"] = serialize_value(endpoint_reference) return self._client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args) + def with_environment(self, name: str, value: str | ReferenceExpression | EndpointReference | ParameterResource | IResourceWithConnectionString) -> IResourceWithEnvironment: + """Sets an environment variable on the resource""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["name"] = serialize_value(name) + args["value"] = serialize_value(value) + return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) + def with_environment_parameter(self, name: str, parameter: ParameterResource) -> IResourceWithEnvironment: """Sets an environment variable from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -3500,7 +3446,7 @@ def wait_for(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to be ready""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitFor", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResource", args) def wait_for_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource with specific behavior""" @@ -3513,7 +3459,7 @@ def wait_for_start(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to start""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitForStart", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceStart", args) def wait_for_start_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource to start with specific behavior""" @@ -3532,7 +3478,7 @@ def wait_for_completion(self, dependency: IResource, exit_code: float = 0) -> IR args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) args["exitCode"] = serialize_value(exit_code) - return self._client.invoke_capability("Aspire.Hosting/waitForCompletion", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args) def with_health_check(self, key: str) -> IResource: """Adds a health check by key""" @@ -3580,7 +3526,7 @@ def with_https_developer_certificate(self, password: ParameterResource | None = args: Dict[str, Any] = { "builder": serialize_value(self._handle) } if password is not None: args["password"] = serialize_value(password) - return self._client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args) def without_https_certificate(self) -> IResourceWithEnvironment: """Removes HTTPS certificate configuration""" @@ -3591,13 +3537,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -3971,13 +3917,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -4914,13 +4860,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -5451,20 +5397,6 @@ def with_required_command(self, command: str, help_link: str | None = None) -> I args["helpLink"] = serialize_value(help_link) return self._client.invoke_capability("Aspire.Hosting/withRequiredCommand", args) - def with_environment(self, name: str, value: str) -> IResourceWithEnvironment: - """Sets an environment variable""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) - - def with_environment_expression(self, name: str, value: ReferenceExpression) -> IResourceWithEnvironment: - """Adds an environment variable with a reference expression""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args) - def with_environment_callback(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: """Sets environment variables via callback""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -5473,14 +5405,6 @@ def with_environment_callback(self, callback: Callable[[EnvironmentCallbackConte args["callback"] = callback_id return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallback", args) - def with_environment_callback_async(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: - """Sets environment variables via async callback""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - callback_id = register_callback(callback) if callback is not None else None - if callback_id is not None: - args["callback"] = callback_id - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args) - def with_environment_endpoint(self, name: str, endpoint_reference: EndpointReference) -> IResourceWithEnvironment: """Sets an environment variable from an endpoint reference""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -5488,6 +5412,13 @@ def with_environment_endpoint(self, name: str, endpoint_reference: EndpointRefer args["endpointReference"] = serialize_value(endpoint_reference) return self._client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args) + def with_environment(self, name: str, value: str | ReferenceExpression | EndpointReference | ParameterResource | IResourceWithConnectionString) -> IResourceWithEnvironment: + """Sets an environment variable on the resource""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["name"] = serialize_value(name) + args["value"] = serialize_value(value) + return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) + def with_environment_parameter(self, name: str, parameter: ParameterResource) -> IResourceWithEnvironment: """Sets an environment variable from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -5673,7 +5604,7 @@ def publish_with_container_files(self, source: IResourceWithContainerFiles, dest args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["source"] = serialize_value(source) args["destinationPath"] = serialize_value(destination_path) - return self._client.invoke_capability("Aspire.Hosting/publishWithContainerFiles", args) + return self._client.invoke_capability("Aspire.Hosting/publishWithContainerFilesFromResource", args) def exclude_from_manifest(self) -> IResource: """Excludes the resource from the deployment manifest""" @@ -5684,7 +5615,7 @@ def wait_for(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to be ready""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitFor", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResource", args) def wait_for_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource with specific behavior""" @@ -5697,7 +5628,7 @@ def wait_for_start(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to start""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitForStart", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceStart", args) def wait_for_start_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource to start with specific behavior""" @@ -5716,7 +5647,7 @@ def wait_for_completion(self, dependency: IResource, exit_code: float = 0) -> IR args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) args["exitCode"] = serialize_value(exit_code) - return self._client.invoke_capability("Aspire.Hosting/waitForCompletion", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args) def with_health_check(self, key: str) -> IResource: """Adds a health check by key""" @@ -5764,7 +5695,7 @@ def with_https_developer_certificate(self, password: ParameterResource | None = args: Dict[str, Any] = { "builder": serialize_value(self._handle) } if password is not None: args["password"] = serialize_value(password) - return self._client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args) def without_https_certificate(self) -> IResourceWithEnvironment: """Removes HTTPS certificate configuration""" @@ -5775,13 +5706,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -6415,14 +6346,14 @@ def with_build_arg(self, name: str, value: ParameterResource) -> ContainerResour args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["name"] = serialize_value(name) args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withBuildArg", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterBuildArg", args) def with_build_secret(self, name: str, value: ParameterResource) -> ContainerResource: """Adds a build secret from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["name"] = serialize_value(name) args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withBuildSecret", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterBuildSecret", args) def with_endpoint_proxy_support(self, proxy_enabled: bool) -> ContainerResource: """Configures endpoint proxy support""" @@ -6477,20 +6408,6 @@ def with_required_command(self, command: str, help_link: str | None = None) -> I args["helpLink"] = serialize_value(help_link) return self._client.invoke_capability("Aspire.Hosting/withRequiredCommand", args) - def with_environment(self, name: str, value: str) -> IResourceWithEnvironment: - """Sets an environment variable""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) - - def with_environment_expression(self, name: str, value: ReferenceExpression) -> IResourceWithEnvironment: - """Adds an environment variable with a reference expression""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args) - def with_environment_callback(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: """Sets environment variables via callback""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -6499,14 +6416,6 @@ def with_environment_callback(self, callback: Callable[[EnvironmentCallbackConte args["callback"] = callback_id return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallback", args) - def with_environment_callback_async(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: - """Sets environment variables via async callback""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - callback_id = register_callback(callback) if callback is not None else None - if callback_id is not None: - args["callback"] = callback_id - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args) - def with_environment_endpoint(self, name: str, endpoint_reference: EndpointReference) -> IResourceWithEnvironment: """Sets an environment variable from an endpoint reference""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -6514,6 +6423,13 @@ def with_environment_endpoint(self, name: str, endpoint_reference: EndpointRefer args["endpointReference"] = serialize_value(endpoint_reference) return self._client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args) + def with_environment(self, name: str, value: str | ReferenceExpression | EndpointReference | ParameterResource | IResourceWithConnectionString) -> IResourceWithEnvironment: + """Sets an environment variable on the resource""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["name"] = serialize_value(name) + args["value"] = serialize_value(value) + return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) + def with_environment_parameter(self, name: str, parameter: ParameterResource) -> IResourceWithEnvironment: """Sets an environment variable from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -6703,7 +6619,7 @@ def wait_for(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to be ready""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitFor", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResource", args) def wait_for_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource with specific behavior""" @@ -6716,7 +6632,7 @@ def wait_for_start(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to start""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitForStart", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceStart", args) def wait_for_start_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource to start with specific behavior""" @@ -6735,7 +6651,7 @@ def wait_for_completion(self, dependency: IResource, exit_code: float = 0) -> IR args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) args["exitCode"] = serialize_value(exit_code) - return self._client.invoke_capability("Aspire.Hosting/waitForCompletion", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args) def with_health_check(self, key: str) -> IResource: """Adds a health check by key""" @@ -6783,7 +6699,7 @@ def with_https_developer_certificate(self, password: ParameterResource | None = args: Dict[str, Any] = { "builder": serialize_value(self._handle) } if password is not None: args["password"] = serialize_value(password) - return self._client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args) def without_https_certificate(self) -> IResourceWithEnvironment: """Removes HTTPS certificate configuration""" @@ -6794,13 +6710,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -7166,14 +7082,14 @@ def with_build_arg(self, name: str, value: ParameterResource) -> ContainerResour args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["name"] = serialize_value(name) args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withBuildArg", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterBuildArg", args) def with_build_secret(self, name: str, value: ParameterResource) -> ContainerResource: """Adds a build secret from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["name"] = serialize_value(name) args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withBuildSecret", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterBuildSecret", args) def with_endpoint_proxy_support(self, proxy_enabled: bool) -> ContainerResource: """Configures endpoint proxy support""" @@ -7228,20 +7144,6 @@ def with_required_command(self, command: str, help_link: str | None = None) -> I args["helpLink"] = serialize_value(help_link) return self._client.invoke_capability("Aspire.Hosting/withRequiredCommand", args) - def with_environment(self, name: str, value: str) -> IResourceWithEnvironment: - """Sets an environment variable""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) - - def with_environment_expression(self, name: str, value: ReferenceExpression) -> IResourceWithEnvironment: - """Adds an environment variable with a reference expression""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args) - def with_environment_callback(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: """Sets environment variables via callback""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -7250,14 +7152,6 @@ def with_environment_callback(self, callback: Callable[[EnvironmentCallbackConte args["callback"] = callback_id return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallback", args) - def with_environment_callback_async(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: - """Sets environment variables via async callback""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - callback_id = register_callback(callback) if callback is not None else None - if callback_id is not None: - args["callback"] = callback_id - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args) - def with_environment_endpoint(self, name: str, endpoint_reference: EndpointReference) -> IResourceWithEnvironment: """Sets an environment variable from an endpoint reference""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -7265,6 +7159,13 @@ def with_environment_endpoint(self, name: str, endpoint_reference: EndpointRefer args["endpointReference"] = serialize_value(endpoint_reference) return self._client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args) + def with_environment(self, name: str, value: str | ReferenceExpression | EndpointReference | ParameterResource | IResourceWithConnectionString) -> IResourceWithEnvironment: + """Sets an environment variable on the resource""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["name"] = serialize_value(name) + args["value"] = serialize_value(value) + return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) + def with_environment_parameter(self, name: str, parameter: ParameterResource) -> IResourceWithEnvironment: """Sets an environment variable from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -7326,6 +7227,12 @@ def with_reference(self, source: IResource, connection_name: str | None = None, args["name"] = serialize_value(name) return self._client.invoke_capability("Aspire.Hosting/withReference", args) + def get_connection_property(self, key: str) -> ReferenceExpression: + """Gets a connection property by key""" + args: Dict[str, Any] = { "resource": serialize_value(self._handle) } + args["key"] = serialize_value(key) + return self._client.invoke_capability("Aspire.Hosting/getConnectionProperty", args) + def with_reference_uri(self, name: str, uri: str) -> IResourceWithEnvironment: """Adds a reference to a URI""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -7468,7 +7375,7 @@ def wait_for(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to be ready""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitFor", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResource", args) def wait_for_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource with specific behavior""" @@ -7481,7 +7388,7 @@ def wait_for_start(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to start""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitForStart", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceStart", args) def wait_for_start_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource to start with specific behavior""" @@ -7500,7 +7407,7 @@ def wait_for_completion(self, dependency: IResource, exit_code: float = 0) -> IR args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) args["exitCode"] = serialize_value(exit_code) - return self._client.invoke_capability("Aspire.Hosting/waitForCompletion", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args) def with_health_check(self, key: str) -> IResource: """Adds a health check by key""" @@ -7548,7 +7455,7 @@ def with_https_developer_certificate(self, password: ParameterResource | None = args: Dict[str, Any] = { "builder": serialize_value(self._handle) } if password is not None: args["password"] = serialize_value(password) - return self._client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args) def without_https_certificate(self) -> IResourceWithEnvironment: """Removes HTTPS certificate configuration""" @@ -7559,13 +7466,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -8037,14 +7944,14 @@ def with_build_arg(self, name: str, value: ParameterResource) -> ContainerResour args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["name"] = serialize_value(name) args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withBuildArg", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterBuildArg", args) def with_build_secret(self, name: str, value: ParameterResource) -> ContainerResource: """Adds a build secret from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["name"] = serialize_value(name) args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withBuildSecret", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterBuildSecret", args) def with_endpoint_proxy_support(self, proxy_enabled: bool) -> ContainerResource: """Configures endpoint proxy support""" @@ -8099,20 +8006,6 @@ def with_required_command(self, command: str, help_link: str | None = None) -> I args["helpLink"] = serialize_value(help_link) return self._client.invoke_capability("Aspire.Hosting/withRequiredCommand", args) - def with_environment(self, name: str, value: str) -> IResourceWithEnvironment: - """Sets an environment variable""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) - - def with_environment_expression(self, name: str, value: ReferenceExpression) -> IResourceWithEnvironment: - """Adds an environment variable with a reference expression""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - args["name"] = serialize_value(name) - args["value"] = serialize_value(value) - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args) - def with_environment_callback(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: """Sets environment variables via callback""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -8121,14 +8014,6 @@ def with_environment_callback(self, callback: Callable[[EnvironmentCallbackConte args["callback"] = callback_id return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallback", args) - def with_environment_callback_async(self, callback: Callable[[EnvironmentCallbackContext], None]) -> IResourceWithEnvironment: - """Sets environment variables via async callback""" - args: Dict[str, Any] = { "builder": serialize_value(self._handle) } - callback_id = register_callback(callback) if callback is not None else None - if callback_id is not None: - args["callback"] = callback_id - return self._client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args) - def with_environment_endpoint(self, name: str, endpoint_reference: EndpointReference) -> IResourceWithEnvironment: """Sets an environment variable from an endpoint reference""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -8136,6 +8021,13 @@ def with_environment_endpoint(self, name: str, endpoint_reference: EndpointRefer args["endpointReference"] = serialize_value(endpoint_reference) return self._client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args) + def with_environment(self, name: str, value: str | ReferenceExpression | EndpointReference | ParameterResource | IResourceWithConnectionString) -> IResourceWithEnvironment: + """Sets an environment variable on the resource""" + args: Dict[str, Any] = { "builder": serialize_value(self._handle) } + args["name"] = serialize_value(name) + args["value"] = serialize_value(value) + return self._client.invoke_capability("Aspire.Hosting/withEnvironment", args) + def with_environment_parameter(self, name: str, parameter: ParameterResource) -> IResourceWithEnvironment: """Sets an environment variable from a parameter resource""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } @@ -8325,7 +8217,7 @@ def wait_for(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to be ready""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitFor", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResource", args) def wait_for_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource with specific behavior""" @@ -8338,7 +8230,7 @@ def wait_for_start(self, dependency: IResource) -> IResourceWithWaitSupport: """Waits for another resource to start""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) - return self._client.invoke_capability("Aspire.Hosting/waitForStart", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceStart", args) def wait_for_start_with_behavior(self, dependency: IResource, wait_behavior: WaitBehavior) -> IResourceWithWaitSupport: """Waits for another resource to start with specific behavior""" @@ -8357,7 +8249,7 @@ def wait_for_completion(self, dependency: IResource, exit_code: float = 0) -> IR args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["dependency"] = serialize_value(dependency) args["exitCode"] = serialize_value(exit_code) - return self._client.invoke_capability("Aspire.Hosting/waitForCompletion", args) + return self._client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args) def with_health_check(self, key: str) -> IResource: """Adds a health check by key""" @@ -8405,7 +8297,7 @@ def with_https_developer_certificate(self, password: ParameterResource | None = args: Dict[str, Any] = { "builder": serialize_value(self._handle) } if password is not None: args["password"] = serialize_value(password) - return self._client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args) + return self._client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args) def without_https_certificate(self) -> IResourceWithEnvironment: """Removes HTTPS certificate configuration""" @@ -8416,13 +8308,13 @@ def with_parent_relationship(self, parent: IResource) -> IResource: """Sets the parent relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["parent"] = serialize_value(parent) - return self._client.invoke_capability("Aspire.Hosting/withParentRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args) def with_child_relationship(self, child: IResource) -> IResource: """Sets a child relationship""" args: Dict[str, Any] = { "builder": serialize_value(self._handle) } args["child"] = serialize_value(child) - return self._client.invoke_capability("Aspire.Hosting/withChildRelationship", args) + return self._client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args) def with_icon_name(self, icon_name: str, icon_variant: IconVariant = None) -> IResource: """Sets the icon for the resource""" @@ -8772,6 +8664,10 @@ def connect() -> AspireClient: raise RuntimeError("REMOTE_APP_HOST_SOCKET_PATH environment variable not set. Run this application using `aspire run`.") client = AspireClient(socket_path) client.connect() + auth_token = os.environ.get("ASPIRE_REMOTE_APPHOST_TOKEN") + if not auth_token: + raise RuntimeError("ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`.") + client.authenticate(auth_token) client.on_disconnect(lambda: sys.exit(1)) return client diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/AtsGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/AtsGeneratedAspire.verified.rs index 9fc7729a4dd..ee7bd5fa177 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/AtsGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/AtsGeneratedAspire.verified.rs @@ -1301,6 +1301,9 @@ pub fn connect() -> Result, Box> { .map_err(|_| "REMOTE_APP_HOST_SOCKET_PATH environment variable not set. Run this application using `aspire run`")?; let client = Arc::new(AspireClient::new(&socket_path)); client.connect()?; + let auth_token = std::env::var("ASPIRE_REMOTE_APPHOST_TOKEN") + .map_err(|_| "ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`")?; + client.authenticate(&auth_token)?; Ok(client) } diff --git a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs index 8002661db95..d1ba5360ef7 100644 --- a/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs +++ b/tests/Aspire.Hosting.CodeGeneration.Rust.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.rs @@ -858,28 +858,6 @@ impl CSharpAppResource { Ok(IResource::new(handle, self.client.clone())) } - /// Sets an environment variable - pub fn with_environment(&self, name: &str, value: &str) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Adds an environment variable with a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -891,24 +869,24 @@ impl CSharpAppResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets environment variables via async callback - pub fn with_environment_callback_async(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { + /// Sets an environment variable from an endpoint reference + pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); - let callback_id = register_callback(callback); - args.insert("callback".to_string(), Value::String(callback_id)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args)?; + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { + /// Sets an environment variable on the resource + pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; + args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -1204,7 +1182,7 @@ impl CSharpAppResource { args.insert("builder".to_string(), self.handle.to_json()); args.insert("source".to_string(), source.handle().to_json()); args.insert("destinationPath".to_string(), serde_json::to_value(&destination_path).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/publishWithContainerFiles", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/publishWithContainerFilesFromResource", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IContainerFilesDestinationResource::new(handle, self.client.clone())) } @@ -1223,7 +1201,7 @@ impl CSharpAppResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitFor", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResource", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -1244,7 +1222,7 @@ impl CSharpAppResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitForStart", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceStart", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -1277,7 +1255,7 @@ impl CSharpAppResource { if let Some(ref v) = exit_code { args.insert("exitCode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - let result = self.client.invoke_capability("Aspire.Hosting/waitForCompletion", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -1353,7 +1331,7 @@ impl CSharpAppResource { if let Some(ref v) = password { args.insert("password".to_string(), v.handle().to_json()); } - let result = self.client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -1372,7 +1350,7 @@ impl CSharpAppResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -1382,7 +1360,7 @@ impl CSharpAppResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -1945,6 +1923,15 @@ impl ConnectionStringResource { Ok(IResourceWithConnectionString::new(handle, self.client.clone())) } + /// Gets a connection property by key + pub fn get_connection_property(&self, key: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("resource".to_string(), self.handle.to_json()); + args.insert("key".to_string(), serde_json::to_value(&key).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/getConnectionProperty", args)?; + Ok(serde_json::from_value(result)?) + } + /// Customizes displayed URLs via callback pub fn with_urls_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -2019,7 +2006,7 @@ impl ConnectionStringResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitFor", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResource", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -2040,7 +2027,7 @@ impl ConnectionStringResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitForStart", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceStart", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -2073,7 +2060,7 @@ impl ConnectionStringResource { if let Some(ref v) = exit_code { args.insert("exitCode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - let result = self.client.invoke_capability("Aspire.Hosting/waitForCompletion", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -2109,7 +2096,7 @@ impl ConnectionStringResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -2119,7 +2106,7 @@ impl ConnectionStringResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -2586,7 +2573,7 @@ impl ContainerRegistryResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -2596,7 +2583,7 @@ impl ContainerRegistryResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -3033,7 +3020,7 @@ impl ContainerResource { args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); args.insert("value".to_string(), value.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withBuildArg", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterBuildArg", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(ContainerResource::new(handle, self.client.clone())) } @@ -3044,7 +3031,7 @@ impl ContainerResource { args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); args.insert("value".to_string(), value.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withBuildSecret", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterBuildSecret", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(ContainerResource::new(handle, self.client.clone())) } @@ -3140,28 +3127,6 @@ impl ContainerResource { Ok(IResource::new(handle, self.client.clone())) } - /// Sets an environment variable - pub fn with_environment(&self, name: &str, value: &str) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Adds an environment variable with a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -3173,24 +3138,24 @@ impl ContainerResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets environment variables via async callback - pub fn with_environment_callback_async(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { + /// Sets an environment variable from an endpoint reference + pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); - let callback_id = register_callback(callback); - args.insert("callback".to_string(), Value::String(callback_id)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args)?; + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { + /// Sets an environment variable on the resource + pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; + args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -3494,7 +3459,7 @@ impl ContainerResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitFor", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResource", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -3515,7 +3480,7 @@ impl ContainerResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitForStart", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceStart", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -3548,7 +3513,7 @@ impl ContainerResource { if let Some(ref v) = exit_code { args.insert("exitCode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - let result = self.client.invoke_capability("Aspire.Hosting/waitForCompletion", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -3624,7 +3589,7 @@ impl ContainerResource { if let Some(ref v) = password { args.insert("password".to_string(), v.handle().to_json()); } - let result = self.client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -3643,7 +3608,7 @@ impl ContainerResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -3653,7 +3618,7 @@ impl ContainerResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -4448,28 +4413,6 @@ impl DotnetToolResource { Ok(IResource::new(handle, self.client.clone())) } - /// Sets an environment variable - pub fn with_environment(&self, name: &str, value: &str) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Adds an environment variable with a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -4481,24 +4424,24 @@ impl DotnetToolResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets environment variables via async callback - pub fn with_environment_callback_async(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { + /// Sets an environment variable from an endpoint reference + pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); - let callback_id = register_callback(callback); - args.insert("callback".to_string(), Value::String(callback_id)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args)?; + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { + /// Sets an environment variable on the resource + pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; + args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -4802,7 +4745,7 @@ impl DotnetToolResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitFor", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResource", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -4823,7 +4766,7 @@ impl DotnetToolResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitForStart", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceStart", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -4856,7 +4799,7 @@ impl DotnetToolResource { if let Some(ref v) = exit_code { args.insert("exitCode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - let result = self.client.invoke_capability("Aspire.Hosting/waitForCompletion", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -4932,7 +4875,7 @@ impl DotnetToolResource { if let Some(ref v) = password { args.insert("password".to_string(), v.handle().to_json()); } - let result = self.client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -4951,7 +4894,7 @@ impl DotnetToolResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -4961,7 +4904,7 @@ impl DotnetToolResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -5736,28 +5679,6 @@ impl ExecutableResource { Ok(IResource::new(handle, self.client.clone())) } - /// Sets an environment variable - pub fn with_environment(&self, name: &str, value: &str) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Adds an environment variable with a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -5769,24 +5690,24 @@ impl ExecutableResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets environment variables via async callback - pub fn with_environment_callback_async(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { + /// Sets an environment variable from an endpoint reference + pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); - let callback_id = register_callback(callback); - args.insert("callback".to_string(), Value::String(callback_id)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args)?; + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { + /// Sets an environment variable on the resource + pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; + args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -6090,7 +6011,7 @@ impl ExecutableResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitFor", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResource", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -6111,7 +6032,7 @@ impl ExecutableResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitForStart", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceStart", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -6144,7 +6065,7 @@ impl ExecutableResource { if let Some(ref v) = exit_code { args.insert("exitCode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - let result = self.client.invoke_capability("Aspire.Hosting/waitForCompletion", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -6220,7 +6141,7 @@ impl ExecutableResource { if let Some(ref v) = password { args.insert("password".to_string(), v.handle().to_json()); } - let result = self.client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -6239,7 +6160,7 @@ impl ExecutableResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -6249,7 +6170,7 @@ impl ExecutableResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -6868,7 +6789,7 @@ impl ExternalServiceResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -6878,7 +6799,7 @@ impl ExternalServiceResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -8778,7 +8699,7 @@ impl ParameterResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -8788,7 +8709,7 @@ impl ParameterResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -9738,28 +9659,6 @@ impl ProjectResource { Ok(IResource::new(handle, self.client.clone())) } - /// Sets an environment variable - pub fn with_environment(&self, name: &str, value: &str) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Adds an environment variable with a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -9771,24 +9670,24 @@ impl ProjectResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets environment variables via async callback - pub fn with_environment_callback_async(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { + /// Sets an environment variable from an endpoint reference + pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); - let callback_id = register_callback(callback); - args.insert("callback".to_string(), Value::String(callback_id)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args)?; + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { + /// Sets an environment variable on the resource + pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; + args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -10084,7 +9983,7 @@ impl ProjectResource { args.insert("builder".to_string(), self.handle.to_json()); args.insert("source".to_string(), source.handle().to_json()); args.insert("destinationPath".to_string(), serde_json::to_value(&destination_path).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/publishWithContainerFiles", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/publishWithContainerFilesFromResource", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IContainerFilesDestinationResource::new(handle, self.client.clone())) } @@ -10103,7 +10002,7 @@ impl ProjectResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitFor", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResource", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -10124,7 +10023,7 @@ impl ProjectResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitForStart", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceStart", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -10157,7 +10056,7 @@ impl ProjectResource { if let Some(ref v) = exit_code { args.insert("exitCode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - let result = self.client.invoke_capability("Aspire.Hosting/waitForCompletion", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -10233,7 +10132,7 @@ impl ProjectResource { if let Some(ref v) = password { args.insert("password".to_string(), v.handle().to_json()); } - let result = self.client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -10252,7 +10151,7 @@ impl ProjectResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -10262,7 +10161,7 @@ impl ProjectResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -11400,7 +11299,7 @@ impl TestDatabaseResource { args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); args.insert("value".to_string(), value.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withBuildArg", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterBuildArg", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(ContainerResource::new(handle, self.client.clone())) } @@ -11411,7 +11310,7 @@ impl TestDatabaseResource { args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); args.insert("value".to_string(), value.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withBuildSecret", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterBuildSecret", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(ContainerResource::new(handle, self.client.clone())) } @@ -11507,28 +11406,6 @@ impl TestDatabaseResource { Ok(IResource::new(handle, self.client.clone())) } - /// Sets an environment variable - pub fn with_environment(&self, name: &str, value: &str) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Adds an environment variable with a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -11540,24 +11417,24 @@ impl TestDatabaseResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets environment variables via async callback - pub fn with_environment_callback_async(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { + /// Sets an environment variable from an endpoint reference + pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); - let callback_id = register_callback(callback); - args.insert("callback".to_string(), Value::String(callback_id)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args)?; + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { + /// Sets an environment variable on the resource + pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; + args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -11861,7 +11738,7 @@ impl TestDatabaseResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitFor", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResource", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -11882,7 +11759,7 @@ impl TestDatabaseResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitForStart", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceStart", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -11915,7 +11792,7 @@ impl TestDatabaseResource { if let Some(ref v) = exit_code { args.insert("exitCode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - let result = self.client.invoke_capability("Aspire.Hosting/waitForCompletion", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -11991,7 +11868,7 @@ impl TestDatabaseResource { if let Some(ref v) = password { args.insert("password".to_string(), v.handle().to_json()); } - let result = self.client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -12010,7 +11887,7 @@ impl TestDatabaseResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -12020,7 +11897,7 @@ impl TestDatabaseResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -12636,7 +12513,7 @@ impl TestRedisResource { args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); args.insert("value".to_string(), value.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withBuildArg", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterBuildArg", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(ContainerResource::new(handle, self.client.clone())) } @@ -12647,7 +12524,7 @@ impl TestRedisResource { args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); args.insert("value".to_string(), value.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withBuildSecret", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterBuildSecret", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(ContainerResource::new(handle, self.client.clone())) } @@ -12743,28 +12620,6 @@ impl TestRedisResource { Ok(IResource::new(handle, self.client.clone())) } - /// Sets an environment variable - pub fn with_environment(&self, name: &str, value: &str) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Adds an environment variable with a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -12776,24 +12631,24 @@ impl TestRedisResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets environment variables via async callback - pub fn with_environment_callback_async(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { + /// Sets an environment variable from an endpoint reference + pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); - let callback_id = register_callback(callback); - args.insert("callback".to_string(), Value::String(callback_id)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args)?; + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { + /// Sets an environment variable on the resource + pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; + args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -12893,6 +12748,15 @@ impl TestRedisResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } + /// Gets a connection property by key + pub fn get_connection_property(&self, key: &str) -> Result> { + let mut args: HashMap = HashMap::new(); + args.insert("resource".to_string(), self.handle.to_json()); + args.insert("key".to_string(), serde_json::to_value(&key).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/getConnectionProperty", args)?; + Ok(serde_json::from_value(result)?) + } + /// Adds a reference to a URI pub fn with_reference_uri(&self, name: &str, uri: &str) -> Result> { let mut args: HashMap = HashMap::new(); @@ -13119,7 +12983,7 @@ impl TestRedisResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitFor", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResource", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -13140,7 +13004,7 @@ impl TestRedisResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitForStart", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceStart", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -13173,7 +13037,7 @@ impl TestRedisResource { if let Some(ref v) = exit_code { args.insert("exitCode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - let result = self.client.invoke_capability("Aspire.Hosting/waitForCompletion", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -13249,7 +13113,7 @@ impl TestRedisResource { if let Some(ref v) = password { args.insert("password".to_string(), v.handle().to_json()); } - let result = self.client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -13268,7 +13132,7 @@ impl TestRedisResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -13278,7 +13142,7 @@ impl TestRedisResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -14036,7 +13900,7 @@ impl TestVaultResource { args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); args.insert("value".to_string(), value.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withBuildArg", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterBuildArg", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(ContainerResource::new(handle, self.client.clone())) } @@ -14047,7 +13911,7 @@ impl TestVaultResource { args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); args.insert("value".to_string(), value.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withBuildSecret", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterBuildSecret", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(ContainerResource::new(handle, self.client.clone())) } @@ -14143,28 +14007,6 @@ impl TestVaultResource { Ok(IResource::new(handle, self.client.clone())) } - /// Sets an environment variable - pub fn with_environment(&self, name: &str, value: &str) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - - /// Adds an environment variable with a reference expression - pub fn with_environment_expression(&self, name: &str, value: ReferenceExpression) -> Result> { - let mut args: HashMap = HashMap::new(); - args.insert("builder".to_string(), self.handle.to_json()); - args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentExpression", args)?; - let handle: Handle = serde_json::from_value(result)?; - Ok(IResourceWithEnvironment::new(handle, self.client.clone())) - } - /// Sets environment variables via callback pub fn with_environment_callback(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { let mut args: HashMap = HashMap::new(); @@ -14176,24 +14018,24 @@ impl TestVaultResource { Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets environment variables via async callback - pub fn with_environment_callback_async(&self, callback: impl Fn(Vec) -> Value + Send + Sync + 'static) -> Result> { + /// Sets an environment variable from an endpoint reference + pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); - let callback_id = register_callback(callback); - args.insert("callback".to_string(), Value::String(callback_id)); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentCallbackAsync", args)?; + args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); + args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } - /// Sets an environment variable from an endpoint reference - pub fn with_environment_endpoint(&self, name: &str, endpoint_reference: &EndpointReference) -> Result> { + /// Sets an environment variable on the resource + pub fn with_environment(&self, name: &str, value: Value) -> Result> { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("name".to_string(), serde_json::to_value(&name).unwrap_or(Value::Null)); - args.insert("endpointReference".to_string(), endpoint_reference.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withEnvironmentEndpoint", args)?; + args.insert("value".to_string(), serde_json::to_value(&value).unwrap_or(Value::Null)); + let result = self.client.invoke_capability("Aspire.Hosting/withEnvironment", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -14497,7 +14339,7 @@ impl TestVaultResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitFor", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResource", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -14518,7 +14360,7 @@ impl TestVaultResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("dependency".to_string(), dependency.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/waitForStart", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceStart", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -14551,7 +14393,7 @@ impl TestVaultResource { if let Some(ref v) = exit_code { args.insert("exitCode".to_string(), serde_json::to_value(v).unwrap_or(Value::Null)); } - let result = self.client.invoke_capability("Aspire.Hosting/waitForCompletion", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/waitForResourceCompletion", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithWaitSupport::new(handle, self.client.clone())) } @@ -14627,7 +14469,7 @@ impl TestVaultResource { if let Some(ref v) = password { args.insert("password".to_string(), v.handle().to_json()); } - let result = self.client.invoke_capability("Aspire.Hosting/withHttpsDeveloperCertificate", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withParameterHttpsDeveloperCertificate", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResourceWithEnvironment::new(handle, self.client.clone())) } @@ -14646,7 +14488,7 @@ impl TestVaultResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("parent".to_string(), parent.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withParentRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderParentRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -14656,7 +14498,7 @@ impl TestVaultResource { let mut args: HashMap = HashMap::new(); args.insert("builder".to_string(), self.handle.to_json()); args.insert("child".to_string(), child.handle().to_json()); - let result = self.client.invoke_capability("Aspire.Hosting/withChildRelationship", args)?; + let result = self.client.invoke_capability("Aspire.Hosting/withBuilderChildRelationship", args)?; let handle: Handle = serde_json::from_value(result)?; Ok(IResource::new(handle, self.client.clone())) } @@ -15093,6 +14935,9 @@ pub fn connect() -> Result, Box> { .map_err(|_| "REMOTE_APP_HOST_SOCKET_PATH environment variable not set. Run this application using `aspire run`")?; let client = Arc::new(AspireClient::new(&socket_path)); client.connect()?; + let auth_token = std::env::var("ASPIRE_REMOTE_APPHOST_TOKEN") + .map_err(|_| "ASPIRE_REMOTE_APPHOST_TOKEN environment variable not set. Run this application using `aspire run`")?; + client.authenticate(&auth_token)?; Ok(client) } diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.JsTests/tests/transport.test.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.JsTests/tests/transport.test.ts index 14e39268a7e..8a157d9ec2c 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.JsTests/tests/transport.test.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.JsTests/tests/transport.test.ts @@ -465,6 +465,36 @@ describe('AspireClient', () => { const client = new AspireClient('nonexistent-pipe-' + Date.now()); await expect(client.connect(500)).rejects.toThrow(); }); + + it('sends the authentication token as a direct parameter during connect', async () => { + const previousToken = process.env.ASPIRE_REMOTE_APPHOST_TOKEN; + const authenticate = vi.fn(async (token: string) => { + expect(token).toBe('secret-token'); + return true; + }); + + process.env.ASPIRE_REMOTE_APPHOST_TOKEN = 'secret-token'; + + const fixture = createPipeFixture({ authenticate }); + await fixture.start(); + + const client = new AspireClient(fixture.clientSocketPath); + + try { + await client.connect(); + await fixture.waitForClient(); + + expect(authenticate).toHaveBeenCalledOnce(); + } finally { + if (previousToken === undefined) { + delete process.env.ASPIRE_REMOTE_APPHOST_TOKEN; + } else { + process.env.ASPIRE_REMOTE_APPHOST_TOKEN = previousToken; + } + + await fixture.cleanup(client); + } + }); }); // ============================================================================ @@ -540,7 +570,7 @@ describe('registerHandleWrapper', () => { * Creates a named-pipe server that speaks JSON-RPC, connects an AspireClient, * and provides a `sendRequest` helper to invoke callbacks on the client side. */ -function createPipeFixture() { +function createPipeFixture(options?: { authenticate?: (token: string) => boolean | Promise }) { const pipeName = `aspire-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`; const pipePath = process.platform === 'win32' ? `\\\\.\\pipe\\${pipeName}` : `/tmp/${pipeName}`; let serverConnection: rpc.MessageConnection | null = null; @@ -563,6 +593,10 @@ function createPipeFixture() { // Handle cancelToken serverConnection.onRequest('cancelToken', () => true); + if (options?.authenticate) { + serverConnection.onRequest('authenticate', options.authenticate); + } + serverConnection.listen(); onClientConnected?.(); }); @@ -591,12 +625,48 @@ function createPipeFixture() { describe('callback invocation protocol', () => { async function connectFixture() { - const fixture = createPipeFixture(); + const previousToken = process.env.ASPIRE_REMOTE_APPHOST_TOKEN; + const testToken = 'callback-test-token'; + process.env.ASPIRE_REMOTE_APPHOST_TOKEN = testToken; + + const fixture = createPipeFixture({ + authenticate: (token: string) => token === testToken, + }); + await fixture.start(); const client = new AspireClient(fixture.clientSocketPath); - await client.connect(); - await fixture.waitForClient(); - return { fixture, client }; + + try { + await client.connect(); + await fixture.waitForClient(); + } + catch (error) + { + if (previousToken === undefined) { + delete process.env.ASPIRE_REMOTE_APPHOST_TOKEN; + } else { + process.env.ASPIRE_REMOTE_APPHOST_TOKEN = previousToken; + } + + await fixture.cleanup(client); + throw error; + } + + return { + fixture: { + ...fixture, + cleanup: async (client: AspireClient) => { + if (previousToken === undefined) { + delete process.env.ASPIRE_REMOTE_APPHOST_TOKEN; + } else { + process.env.ASPIRE_REMOTE_APPHOST_TOKEN = previousToken; + } + + await fixture.cleanup(client); + }, + }, + client, + }; } it('invokes a no-arg callback', async () => { diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt index 367388398f6..b287abc55c8 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/HostingContainerResourceCapabilities.verified.txt @@ -168,7 +168,7 @@ ] }, { - CapabilityId: Aspire.Hosting/waitFor, + CapabilityId: Aspire.Hosting/waitForResource, MethodName: waitFor, TargetType: { TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithWaitSupport, @@ -182,7 +182,7 @@ ] }, { - CapabilityId: Aspire.Hosting/waitForCompletion, + CapabilityId: Aspire.Hosting/waitForResourceCompletion, MethodName: waitForCompletion, TargetType: { TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithWaitSupport, @@ -196,7 +196,7 @@ ] }, { - CapabilityId: Aspire.Hosting/waitForStart, + CapabilityId: Aspire.Hosting/waitForResourceStart, MethodName: waitForStart, TargetType: { TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithWaitSupport, @@ -294,11 +294,11 @@ ] }, { - CapabilityId: Aspire.Hosting/withBuildArg, - MethodName: withBuildArg, + CapabilityId: Aspire.Hosting/withBuilderChildRelationship, + MethodName: withChildRelationship, TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource, + IsInterface: true }, ExpandedTargetTypes: [ { @@ -308,11 +308,11 @@ ] }, { - CapabilityId: Aspire.Hosting/withBuildSecret, - MethodName: withBuildSecret, + CapabilityId: Aspire.Hosting/withBuilderParentRelationship, + MethodName: withParentRelationship, TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource, + IsInterface: true }, ExpandedTargetTypes: [ { @@ -335,20 +335,6 @@ } ] }, - { - CapabilityId: Aspire.Hosting/withChildRelationship, - MethodName: withChildRelationship, - TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource, - IsInterface: true - }, - ExpandedTargetTypes: [ - { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - } - ] - }, { CapabilityId: Aspire.Hosting/withCommand, MethodName: withCommand, @@ -531,20 +517,6 @@ } ] }, - { - CapabilityId: Aspire.Hosting/withEnvironmentCallbackAsync, - MethodName: withEnvironmentCallbackAsync, - TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment, - IsInterface: true - }, - ExpandedTargetTypes: [ - { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - } - ] - }, { CapabilityId: Aspire.Hosting/withEnvironmentConnectionString, MethodName: withEnvironmentConnectionString, @@ -573,20 +545,6 @@ } ] }, - { - CapabilityId: Aspire.Hosting/withEnvironmentExpression, - MethodName: withEnvironmentExpression, - TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment, - IsInterface: true - }, - ExpandedTargetTypes: [ - { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - } - ] - }, { CapabilityId: Aspire.Hosting/withEnvironmentParameter, MethodName: withEnvironmentParameter, @@ -685,20 +643,6 @@ } ] }, - { - CapabilityId: Aspire.Hosting/withHttpsDeveloperCertificate, - MethodName: withHttpsDeveloperCertificate, - TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment, - IsInterface: true - }, - ExpandedTargetTypes: [ - { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, - IsInterface: false - } - ] - }, { CapabilityId: Aspire.Hosting/withHttpsEndpoint, MethodName: withHttpsEndpoint, @@ -868,10 +812,38 @@ ] }, { - CapabilityId: Aspire.Hosting/withParentRelationship, - MethodName: withParentRelationship, + CapabilityId: Aspire.Hosting/withParameterBuildArg, + MethodName: withBuildArg, TargetType: { - TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResource, + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + }, + ExpandedTargetTypes: [ + { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + } + ] + }, + { + CapabilityId: Aspire.Hosting/withParameterBuildSecret, + MethodName: withBuildSecret, + TargetType: { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + }, + ExpandedTargetTypes: [ + { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.ContainerResource, + IsInterface: false + } + ] + }, + { + CapabilityId: Aspire.Hosting/withParameterHttpsDeveloperCertificate, + MethodName: withHttpsDeveloperCertificate, + TargetType: { + TypeId: Aspire.Hosting/Aspire.Hosting.ApplicationModel.IResourceWithEnvironment, IsInterface: true }, ExpandedTargetTypes: [ diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts index e96893861b9..11d33822cad 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/TwoPassScanningGeneratedAspire.verified.ts @@ -4623,6 +4623,15 @@ export class ConnectionStringResource extends ResourceBuilderBase { + const rpcArgs: Record = { resource: this._handle, key }; + return await this._client.invokeCapability( + 'Aspire.Hosting/getConnectionProperty', + rpcArgs + ); + } + /** @internal */ private async _withUrlsCallbackInternal(callback: (obj: ResourceUrlsCallbackContext) => Promise): Promise { const callbackId = registerCallback(async (objData: unknown) => { @@ -4735,7 +4744,7 @@ export class ConnectionStringResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitFor', + 'Aspire.Hosting/waitForResource', rpcArgs ); return new ConnectionStringResource(result, this._client); @@ -4765,7 +4774,7 @@ export class ConnectionStringResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForStart', + 'Aspire.Hosting/waitForResourceStart', rpcArgs ); return new ConnectionStringResource(result, this._client); @@ -4811,7 +4820,7 @@ export class ConnectionStringResource extends ResourceBuilderBase = { builder: this._handle, dependency }; if (exitCode !== undefined) rpcArgs.exitCode = exitCode; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForCompletion', + 'Aspire.Hosting/waitForResourceCompletion', rpcArgs ); return new ConnectionStringResource(result, this._client); @@ -4864,7 +4873,7 @@ export class ConnectionStringResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new ConnectionStringResource(result, this._client); @@ -4879,7 +4888,7 @@ export class ConnectionStringResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new ConnectionStringResource(result, this._client); @@ -5386,6 +5395,11 @@ export class ConnectionStringResourcePromise implements PromiseLike obj.withConnectionPropertyValue(name, value))); } + /** Gets a connection property by key */ + getConnectionProperty(key: string): Promise { + return this._promise.then(obj => obj.getConnectionProperty(key)); + } + /** Customizes displayed URLs via callback */ withUrlsCallback(callback: (obj: ResourceUrlsCallbackContext) => Promise): ConnectionStringResourcePromise { return new ConnectionStringResourcePromise(this._promise.then(obj => obj.withUrlsCallback(callback))); @@ -5822,7 +5836,7 @@ export class ContainerRegistryResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new ContainerRegistryResource(result, this._client); @@ -5837,7 +5851,7 @@ export class ContainerRegistryResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new ContainerRegistryResource(result, this._client); @@ -6672,7 +6686,7 @@ export class ContainerResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBuildArg', + 'Aspire.Hosting/withParameterBuildArg', rpcArgs ); return new ContainerResource(result, this._client); @@ -6687,7 +6701,7 @@ export class ContainerResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBuildSecret', + 'Aspire.Hosting/withParameterBuildSecret', rpcArgs ); return new ContainerResource(result, this._client); @@ -6829,41 +6843,11 @@ export class ContainerResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironment', - rpcArgs - ); - return new ContainerResource(result, this._client); - } - - /** Sets an environment variable */ - withEnvironment(name: string, value: string): ContainerResourcePromise { - return new ContainerResourcePromise(this._withEnvironmentInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new ContainerResource(result, this._client); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ContainerResourcePromise { - return new ContainerResourcePromise(this._withEnvironmentExpressionInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentCallbackInternal(callback: (obj: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (objData: unknown) => { - const objHandle = wrapIfHandle(objData) as EnvironmentCallbackContextHandle; - const obj = new EnvironmentCallbackContext(objHandle, this._client); - await callback(obj); + private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; + const arg = new EnvironmentCallbackContext(argHandle, this._client); + await callback(arg); }); const rpcArgs: Record = { builder: this._handle, callback: callbackId }; const result = await this._client.invokeCapability( @@ -6874,43 +6858,38 @@ export class ContainerResource extends ResourceBuilderBase Promise): ContainerResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ContainerResourcePromise { return new ContainerResourcePromise(this._withEnvironmentCallbackInternal(callback)); } /** @internal */ - private async _withEnvironmentCallbackAsyncInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (argData: unknown) => { - const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; - const arg = new EnvironmentCallbackContext(argHandle, this._client); - await callback(arg); - }); - const rpcArgs: Record = { builder: this._handle, callback: callbackId }; + private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { + const rpcArgs: Record = { builder: this._handle, name, endpointReference }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentCallbackAsync', + 'Aspire.Hosting/withEnvironmentEndpoint', rpcArgs ); return new ContainerResource(result, this._client); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): ContainerResourcePromise { - return new ContainerResourcePromise(this._withEnvironmentCallbackAsyncInternal(callback)); + /** Sets an environment variable from an endpoint reference */ + withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ContainerResourcePromise { + return new ContainerResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); } /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; + private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): Promise { + const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', + 'Aspire.Hosting/withEnvironment', rpcArgs ); return new ContainerResource(result, this._client); } - /** Sets an environment variable from an endpoint reference */ - withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ContainerResourcePromise { - return new ContainerResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): ContainerResourcePromise { + return new ContainerResourcePromise(this._withEnvironmentInternal(name, value)); } /** @internal */ @@ -7316,7 +7295,7 @@ export class ContainerResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitFor', + 'Aspire.Hosting/waitForResource', rpcArgs ); return new ContainerResource(result, this._client); @@ -7346,7 +7325,7 @@ export class ContainerResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForStart', + 'Aspire.Hosting/waitForResourceStart', rpcArgs ); return new ContainerResource(result, this._client); @@ -7392,7 +7371,7 @@ export class ContainerResource extends ResourceBuilderBase = { builder: this._handle, dependency }; if (exitCode !== undefined) rpcArgs.exitCode = exitCode; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForCompletion', + 'Aspire.Hosting/waitForResourceCompletion', rpcArgs ); return new ContainerResource(result, this._client); @@ -7497,7 +7476,7 @@ export class ContainerResource extends ResourceBuilderBase = { builder: this._handle }; if (password !== undefined) rpcArgs.password = password; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withHttpsDeveloperCertificate', + 'Aspire.Hosting/withParameterHttpsDeveloperCertificate', rpcArgs ); return new ContainerResource(result, this._client); @@ -7528,7 +7507,7 @@ export class ContainerResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new ContainerResource(result, this._client); @@ -7543,7 +7522,7 @@ export class ContainerResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new ContainerResource(result, this._client); @@ -8223,31 +8202,21 @@ export class ContainerResourcePromise implements PromiseLike return new ContainerResourcePromise(this._promise.then(obj => obj.withRequiredCommand(command, options))); } - /** Sets an environment variable */ - withEnvironment(name: string, value: string): ContainerResourcePromise { - return new ContainerResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ContainerResourcePromise { - return new ContainerResourcePromise(this._promise.then(obj => obj.withEnvironmentExpression(name, value))); - } - /** Sets environment variables via callback */ - withEnvironmentCallback(callback: (obj: EnvironmentCallbackContext) => Promise): ContainerResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ContainerResourcePromise { return new ContainerResourcePromise(this._promise.then(obj => obj.withEnvironmentCallback(callback))); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): ContainerResourcePromise { - return new ContainerResourcePromise(this._promise.then(obj => obj.withEnvironmentCallbackAsync(callback))); - } - /** Sets an environment variable from an endpoint reference */ withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ContainerResourcePromise { return new ContainerResourcePromise(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference))); } + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): ContainerResourcePromise { + return new ContainerResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); + } + /** Sets an environment variable from a parameter resource */ withEnvironmentParameter(name: string, parameter: ParameterResource): ContainerResourcePromise { return new ContainerResourcePromise(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter))); @@ -8747,41 +8716,11 @@ export class CSharpAppResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironment', - rpcArgs - ); - return new CSharpAppResource(result, this._client); - } - - /** Sets an environment variable */ - withEnvironment(name: string, value: string): CSharpAppResourcePromise { - return new CSharpAppResourcePromise(this._withEnvironmentInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new CSharpAppResource(result, this._client); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): CSharpAppResourcePromise { - return new CSharpAppResourcePromise(this._withEnvironmentExpressionInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentCallbackInternal(callback: (obj: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (objData: unknown) => { - const objHandle = wrapIfHandle(objData) as EnvironmentCallbackContextHandle; - const obj = new EnvironmentCallbackContext(objHandle, this._client); - await callback(obj); + private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; + const arg = new EnvironmentCallbackContext(argHandle, this._client); + await callback(arg); }); const rpcArgs: Record = { builder: this._handle, callback: callbackId }; const result = await this._client.invokeCapability( @@ -8792,43 +8731,38 @@ export class CSharpAppResource extends ResourceBuilderBase Promise): CSharpAppResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): CSharpAppResourcePromise { return new CSharpAppResourcePromise(this._withEnvironmentCallbackInternal(callback)); } /** @internal */ - private async _withEnvironmentCallbackAsyncInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (argData: unknown) => { - const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; - const arg = new EnvironmentCallbackContext(argHandle, this._client); - await callback(arg); - }); - const rpcArgs: Record = { builder: this._handle, callback: callbackId }; + private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { + const rpcArgs: Record = { builder: this._handle, name, endpointReference }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentCallbackAsync', + 'Aspire.Hosting/withEnvironmentEndpoint', rpcArgs ); return new CSharpAppResource(result, this._client); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): CSharpAppResourcePromise { - return new CSharpAppResourcePromise(this._withEnvironmentCallbackAsyncInternal(callback)); + /** Sets an environment variable from an endpoint reference */ + withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): CSharpAppResourcePromise { + return new CSharpAppResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); } /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; + private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): Promise { + const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', + 'Aspire.Hosting/withEnvironment', rpcArgs ); return new CSharpAppResource(result, this._client); } - /** Sets an environment variable from an endpoint reference */ - withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): CSharpAppResourcePromise { - return new CSharpAppResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): CSharpAppResourcePromise { + return new CSharpAppResourcePromise(this._withEnvironmentInternal(name, value)); } /** @internal */ @@ -9219,7 +9153,7 @@ export class CSharpAppResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, source, destinationPath }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/publishWithContainerFiles', + 'Aspire.Hosting/publishWithContainerFilesFromResource', rpcArgs ); return new CSharpAppResource(result, this._client); @@ -9249,7 +9183,7 @@ export class CSharpAppResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitFor', + 'Aspire.Hosting/waitForResource', rpcArgs ); return new CSharpAppResource(result, this._client); @@ -9279,7 +9213,7 @@ export class CSharpAppResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForStart', + 'Aspire.Hosting/waitForResourceStart', rpcArgs ); return new CSharpAppResource(result, this._client); @@ -9325,7 +9259,7 @@ export class CSharpAppResource extends ResourceBuilderBase = { builder: this._handle, dependency }; if (exitCode !== undefined) rpcArgs.exitCode = exitCode; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForCompletion', + 'Aspire.Hosting/waitForResourceCompletion', rpcArgs ); return new CSharpAppResource(result, this._client); @@ -9430,7 +9364,7 @@ export class CSharpAppResource extends ResourceBuilderBase = { builder: this._handle }; if (password !== undefined) rpcArgs.password = password; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withHttpsDeveloperCertificate', + 'Aspire.Hosting/withParameterHttpsDeveloperCertificate', rpcArgs ); return new CSharpAppResource(result, this._client); @@ -9461,7 +9395,7 @@ export class CSharpAppResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new CSharpAppResource(result, this._client); @@ -9476,7 +9410,7 @@ export class CSharpAppResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new CSharpAppResource(result, this._client); @@ -10067,31 +10001,21 @@ export class CSharpAppResourcePromise implements PromiseLike return new CSharpAppResourcePromise(this._promise.then(obj => obj.withRequiredCommand(command, options))); } - /** Sets an environment variable */ - withEnvironment(name: string, value: string): CSharpAppResourcePromise { - return new CSharpAppResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): CSharpAppResourcePromise { - return new CSharpAppResourcePromise(this._promise.then(obj => obj.withEnvironmentExpression(name, value))); - } - /** Sets environment variables via callback */ - withEnvironmentCallback(callback: (obj: EnvironmentCallbackContext) => Promise): CSharpAppResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): CSharpAppResourcePromise { return new CSharpAppResourcePromise(this._promise.then(obj => obj.withEnvironmentCallback(callback))); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): CSharpAppResourcePromise { - return new CSharpAppResourcePromise(this._promise.then(obj => obj.withEnvironmentCallbackAsync(callback))); - } - /** Sets an environment variable from an endpoint reference */ withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): CSharpAppResourcePromise { return new CSharpAppResourcePromise(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference))); } + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): CSharpAppResourcePromise { + return new CSharpAppResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); + } + /** Sets an environment variable from a parameter resource */ withEnvironmentParameter(name: string, parameter: ParameterResource): CSharpAppResourcePromise { return new CSharpAppResourcePromise(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter))); @@ -10694,41 +10618,11 @@ export class DotnetToolResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironment', - rpcArgs - ); - return new DotnetToolResource(result, this._client); - } - - /** Sets an environment variable */ - withEnvironment(name: string, value: string): DotnetToolResourcePromise { - return new DotnetToolResourcePromise(this._withEnvironmentInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new DotnetToolResource(result, this._client); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): DotnetToolResourcePromise { - return new DotnetToolResourcePromise(this._withEnvironmentExpressionInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentCallbackInternal(callback: (obj: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (objData: unknown) => { - const objHandle = wrapIfHandle(objData) as EnvironmentCallbackContextHandle; - const obj = new EnvironmentCallbackContext(objHandle, this._client); - await callback(obj); + private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; + const arg = new EnvironmentCallbackContext(argHandle, this._client); + await callback(arg); }); const rpcArgs: Record = { builder: this._handle, callback: callbackId }; const result = await this._client.invokeCapability( @@ -10739,43 +10633,38 @@ export class DotnetToolResource extends ResourceBuilderBase Promise): DotnetToolResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): DotnetToolResourcePromise { return new DotnetToolResourcePromise(this._withEnvironmentCallbackInternal(callback)); } /** @internal */ - private async _withEnvironmentCallbackAsyncInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (argData: unknown) => { - const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; - const arg = new EnvironmentCallbackContext(argHandle, this._client); - await callback(arg); - }); - const rpcArgs: Record = { builder: this._handle, callback: callbackId }; + private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { + const rpcArgs: Record = { builder: this._handle, name, endpointReference }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentCallbackAsync', + 'Aspire.Hosting/withEnvironmentEndpoint', rpcArgs ); return new DotnetToolResource(result, this._client); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): DotnetToolResourcePromise { - return new DotnetToolResourcePromise(this._withEnvironmentCallbackAsyncInternal(callback)); + /** Sets an environment variable from an endpoint reference */ + withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): DotnetToolResourcePromise { + return new DotnetToolResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); } /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; + private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): Promise { + const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', + 'Aspire.Hosting/withEnvironment', rpcArgs ); return new DotnetToolResource(result, this._client); } - /** Sets an environment variable from an endpoint reference */ - withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): DotnetToolResourcePromise { - return new DotnetToolResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): DotnetToolResourcePromise { + return new DotnetToolResourcePromise(this._withEnvironmentInternal(name, value)); } /** @internal */ @@ -11181,7 +11070,7 @@ export class DotnetToolResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitFor', + 'Aspire.Hosting/waitForResource', rpcArgs ); return new DotnetToolResource(result, this._client); @@ -11211,7 +11100,7 @@ export class DotnetToolResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForStart', + 'Aspire.Hosting/waitForResourceStart', rpcArgs ); return new DotnetToolResource(result, this._client); @@ -11257,7 +11146,7 @@ export class DotnetToolResource extends ResourceBuilderBase = { builder: this._handle, dependency }; if (exitCode !== undefined) rpcArgs.exitCode = exitCode; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForCompletion', + 'Aspire.Hosting/waitForResourceCompletion', rpcArgs ); return new DotnetToolResource(result, this._client); @@ -11362,7 +11251,7 @@ export class DotnetToolResource extends ResourceBuilderBase = { builder: this._handle }; if (password !== undefined) rpcArgs.password = password; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withHttpsDeveloperCertificate', + 'Aspire.Hosting/withParameterHttpsDeveloperCertificate', rpcArgs ); return new DotnetToolResource(result, this._client); @@ -11393,7 +11282,7 @@ export class DotnetToolResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new DotnetToolResource(result, this._client); @@ -11408,7 +11297,7 @@ export class DotnetToolResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new DotnetToolResource(result, this._client); @@ -12034,31 +11923,21 @@ export class DotnetToolResourcePromise implements PromiseLike obj.withRequiredCommand(command, options))); } - /** Sets an environment variable */ - withEnvironment(name: string, value: string): DotnetToolResourcePromise { - return new DotnetToolResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): DotnetToolResourcePromise { - return new DotnetToolResourcePromise(this._promise.then(obj => obj.withEnvironmentExpression(name, value))); - } - /** Sets environment variables via callback */ - withEnvironmentCallback(callback: (obj: EnvironmentCallbackContext) => Promise): DotnetToolResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): DotnetToolResourcePromise { return new DotnetToolResourcePromise(this._promise.then(obj => obj.withEnvironmentCallback(callback))); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): DotnetToolResourcePromise { - return new DotnetToolResourcePromise(this._promise.then(obj => obj.withEnvironmentCallbackAsync(callback))); - } - /** Sets an environment variable from an endpoint reference */ withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): DotnetToolResourcePromise { return new DotnetToolResourcePromise(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference))); } + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): DotnetToolResourcePromise { + return new DotnetToolResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); + } + /** Sets an environment variable from a parameter resource */ withEnvironmentParameter(name: string, parameter: ParameterResource): DotnetToolResourcePromise { return new DotnetToolResourcePromise(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter))); @@ -12566,41 +12445,11 @@ export class ExecutableResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironment', - rpcArgs - ); - return new ExecutableResource(result, this._client); - } - - /** Sets an environment variable */ - withEnvironment(name: string, value: string): ExecutableResourcePromise { - return new ExecutableResourcePromise(this._withEnvironmentInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new ExecutableResource(result, this._client); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ExecutableResourcePromise { - return new ExecutableResourcePromise(this._withEnvironmentExpressionInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentCallbackInternal(callback: (obj: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (objData: unknown) => { - const objHandle = wrapIfHandle(objData) as EnvironmentCallbackContextHandle; - const obj = new EnvironmentCallbackContext(objHandle, this._client); - await callback(obj); + private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; + const arg = new EnvironmentCallbackContext(argHandle, this._client); + await callback(arg); }); const rpcArgs: Record = { builder: this._handle, callback: callbackId }; const result = await this._client.invokeCapability( @@ -12611,43 +12460,38 @@ export class ExecutableResource extends ResourceBuilderBase Promise): ExecutableResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ExecutableResourcePromise { return new ExecutableResourcePromise(this._withEnvironmentCallbackInternal(callback)); } /** @internal */ - private async _withEnvironmentCallbackAsyncInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (argData: unknown) => { - const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; - const arg = new EnvironmentCallbackContext(argHandle, this._client); - await callback(arg); - }); - const rpcArgs: Record = { builder: this._handle, callback: callbackId }; + private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { + const rpcArgs: Record = { builder: this._handle, name, endpointReference }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentCallbackAsync', + 'Aspire.Hosting/withEnvironmentEndpoint', rpcArgs ); return new ExecutableResource(result, this._client); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): ExecutableResourcePromise { - return new ExecutableResourcePromise(this._withEnvironmentCallbackAsyncInternal(callback)); + /** Sets an environment variable from an endpoint reference */ + withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ExecutableResourcePromise { + return new ExecutableResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); } /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; + private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): Promise { + const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', + 'Aspire.Hosting/withEnvironment', rpcArgs ); return new ExecutableResource(result, this._client); } - /** Sets an environment variable from an endpoint reference */ - withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ExecutableResourcePromise { - return new ExecutableResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): ExecutableResourcePromise { + return new ExecutableResourcePromise(this._withEnvironmentInternal(name, value)); } /** @internal */ @@ -13053,7 +12897,7 @@ export class ExecutableResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitFor', + 'Aspire.Hosting/waitForResource', rpcArgs ); return new ExecutableResource(result, this._client); @@ -13083,7 +12927,7 @@ export class ExecutableResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForStart', + 'Aspire.Hosting/waitForResourceStart', rpcArgs ); return new ExecutableResource(result, this._client); @@ -13129,7 +12973,7 @@ export class ExecutableResource extends ResourceBuilderBase = { builder: this._handle, dependency }; if (exitCode !== undefined) rpcArgs.exitCode = exitCode; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForCompletion', + 'Aspire.Hosting/waitForResourceCompletion', rpcArgs ); return new ExecutableResource(result, this._client); @@ -13234,7 +13078,7 @@ export class ExecutableResource extends ResourceBuilderBase = { builder: this._handle }; if (password !== undefined) rpcArgs.password = password; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withHttpsDeveloperCertificate', + 'Aspire.Hosting/withParameterHttpsDeveloperCertificate', rpcArgs ); return new ExecutableResource(result, this._client); @@ -13265,7 +13109,7 @@ export class ExecutableResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new ExecutableResource(result, this._client); @@ -13280,7 +13124,7 @@ export class ExecutableResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new ExecutableResource(result, this._client); @@ -13876,31 +13720,21 @@ export class ExecutableResourcePromise implements PromiseLike obj.withRequiredCommand(command, options))); } - /** Sets an environment variable */ - withEnvironment(name: string, value: string): ExecutableResourcePromise { - return new ExecutableResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ExecutableResourcePromise { - return new ExecutableResourcePromise(this._promise.then(obj => obj.withEnvironmentExpression(name, value))); - } - /** Sets environment variables via callback */ - withEnvironmentCallback(callback: (obj: EnvironmentCallbackContext) => Promise): ExecutableResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ExecutableResourcePromise { return new ExecutableResourcePromise(this._promise.then(obj => obj.withEnvironmentCallback(callback))); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): ExecutableResourcePromise { - return new ExecutableResourcePromise(this._promise.then(obj => obj.withEnvironmentCallbackAsync(callback))); - } - /** Sets an environment variable from an endpoint reference */ withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ExecutableResourcePromise { return new ExecutableResourcePromise(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference))); } + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): ExecutableResourcePromise { + return new ExecutableResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); + } + /** Sets an environment variable from a parameter resource */ withEnvironmentParameter(name: string, parameter: ParameterResource): ExecutableResourcePromise { return new ExecutableResourcePromise(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter))); @@ -14476,7 +14310,7 @@ export class ExternalServiceResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new ExternalServiceResource(result, this._client); @@ -14491,7 +14325,7 @@ export class ExternalServiceResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new ExternalServiceResource(result, this._client); @@ -15356,7 +15190,7 @@ export class ParameterResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new ParameterResource(result, this._client); @@ -15371,7 +15205,7 @@ export class ParameterResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new ParameterResource(result, this._client); @@ -16157,88 +15991,53 @@ export class ProjectResource extends ResourceBuilderBase } /** @internal */ - private async _withEnvironmentInternal(name: string, value: string): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; + private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; + const arg = new EnvironmentCallbackContext(argHandle, this._client); + await callback(arg); + }); + const rpcArgs: Record = { builder: this._handle, callback: callbackId }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironment', + 'Aspire.Hosting/withEnvironmentCallback', rpcArgs ); return new ProjectResource(result, this._client); } - /** Sets an environment variable */ - withEnvironment(name: string, value: string): ProjectResourcePromise { - return new ProjectResourcePromise(this._withEnvironmentInternal(name, value)); + /** Sets environment variables via callback */ + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ProjectResourcePromise { + return new ProjectResourcePromise(this._withEnvironmentCallbackInternal(callback)); } /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new ProjectResource(result, this._client); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ProjectResourcePromise { - return new ProjectResourcePromise(this._withEnvironmentExpressionInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentCallbackInternal(callback: (obj: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (objData: unknown) => { - const objHandle = wrapIfHandle(objData) as EnvironmentCallbackContextHandle; - const obj = new EnvironmentCallbackContext(objHandle, this._client); - await callback(obj); - }); - const rpcArgs: Record = { builder: this._handle, callback: callbackId }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentCallback', - rpcArgs - ); - return new ProjectResource(result, this._client); - } - - /** Sets environment variables via callback */ - withEnvironmentCallback(callback: (obj: EnvironmentCallbackContext) => Promise): ProjectResourcePromise { - return new ProjectResourcePromise(this._withEnvironmentCallbackInternal(callback)); - } - - /** @internal */ - private async _withEnvironmentCallbackAsyncInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (argData: unknown) => { - const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; - const arg = new EnvironmentCallbackContext(argHandle, this._client); - await callback(arg); - }); - const rpcArgs: Record = { builder: this._handle, callback: callbackId }; + private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { + const rpcArgs: Record = { builder: this._handle, name, endpointReference }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentCallbackAsync', + 'Aspire.Hosting/withEnvironmentEndpoint', rpcArgs ); return new ProjectResource(result, this._client); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): ProjectResourcePromise { - return new ProjectResourcePromise(this._withEnvironmentCallbackAsyncInternal(callback)); + /** Sets an environment variable from an endpoint reference */ + withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ProjectResourcePromise { + return new ProjectResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); } /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; + private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): Promise { + const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', + 'Aspire.Hosting/withEnvironment', rpcArgs ); return new ProjectResource(result, this._client); } - /** Sets an environment variable from an endpoint reference */ - withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ProjectResourcePromise { - return new ProjectResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): ProjectResourcePromise { + return new ProjectResourcePromise(this._withEnvironmentInternal(name, value)); } /** @internal */ @@ -16629,7 +16428,7 @@ export class ProjectResource extends ResourceBuilderBase private async _publishWithContainerFilesInternal(source: ResourceBuilderBase, destinationPath: string): Promise { const rpcArgs: Record = { builder: this._handle, source, destinationPath }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/publishWithContainerFiles', + 'Aspire.Hosting/publishWithContainerFilesFromResource', rpcArgs ); return new ProjectResource(result, this._client); @@ -16659,7 +16458,7 @@ export class ProjectResource extends ResourceBuilderBase private async _waitForInternal(dependency: ResourceBuilderBase): Promise { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitFor', + 'Aspire.Hosting/waitForResource', rpcArgs ); return new ProjectResource(result, this._client); @@ -16689,7 +16488,7 @@ export class ProjectResource extends ResourceBuilderBase private async _waitForStartInternal(dependency: ResourceBuilderBase): Promise { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForStart', + 'Aspire.Hosting/waitForResourceStart', rpcArgs ); return new ProjectResource(result, this._client); @@ -16735,7 +16534,7 @@ export class ProjectResource extends ResourceBuilderBase const rpcArgs: Record = { builder: this._handle, dependency }; if (exitCode !== undefined) rpcArgs.exitCode = exitCode; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForCompletion', + 'Aspire.Hosting/waitForResourceCompletion', rpcArgs ); return new ProjectResource(result, this._client); @@ -16840,7 +16639,7 @@ export class ProjectResource extends ResourceBuilderBase const rpcArgs: Record = { builder: this._handle }; if (password !== undefined) rpcArgs.password = password; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withHttpsDeveloperCertificate', + 'Aspire.Hosting/withParameterHttpsDeveloperCertificate', rpcArgs ); return new ProjectResource(result, this._client); @@ -16871,7 +16670,7 @@ export class ProjectResource extends ResourceBuilderBase private async _withParentRelationshipInternal(parent: ResourceBuilderBase): Promise { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new ProjectResource(result, this._client); @@ -16886,7 +16685,7 @@ export class ProjectResource extends ResourceBuilderBase private async _withChildRelationshipInternal(child: ResourceBuilderBase): Promise { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new ProjectResource(result, this._client); @@ -17477,31 +17276,21 @@ export class ProjectResourcePromise implements PromiseLike { return new ProjectResourcePromise(this._promise.then(obj => obj.withRequiredCommand(command, options))); } - /** Sets an environment variable */ - withEnvironment(name: string, value: string): ProjectResourcePromise { - return new ProjectResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ProjectResourcePromise { - return new ProjectResourcePromise(this._promise.then(obj => obj.withEnvironmentExpression(name, value))); - } - /** Sets environment variables via callback */ - withEnvironmentCallback(callback: (obj: EnvironmentCallbackContext) => Promise): ProjectResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ProjectResourcePromise { return new ProjectResourcePromise(this._promise.then(obj => obj.withEnvironmentCallback(callback))); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): ProjectResourcePromise { - return new ProjectResourcePromise(this._promise.then(obj => obj.withEnvironmentCallbackAsync(callback))); - } - /** Sets an environment variable from an endpoint reference */ withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ProjectResourcePromise { return new ProjectResourcePromise(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference))); } + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): ProjectResourcePromise { + return new ProjectResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); + } + /** Sets an environment variable from a parameter resource */ withEnvironmentParameter(name: string, parameter: ParameterResource): ProjectResourcePromise { return new ProjectResourcePromise(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter))); @@ -18055,7 +17844,7 @@ export class TestDatabaseResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBuildArg', + 'Aspire.Hosting/withParameterBuildArg', rpcArgs ); return new TestDatabaseResource(result, this._client); @@ -18070,7 +17859,7 @@ export class TestDatabaseResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBuildSecret', + 'Aspire.Hosting/withParameterBuildSecret', rpcArgs ); return new TestDatabaseResource(result, this._client); @@ -18212,41 +18001,11 @@ export class TestDatabaseResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironment', - rpcArgs - ); - return new TestDatabaseResource(result, this._client); - } - - /** Sets an environment variable */ - withEnvironment(name: string, value: string): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromise(this._withEnvironmentInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new TestDatabaseResource(result, this._client); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromise(this._withEnvironmentExpressionInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentCallbackInternal(callback: (obj: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (objData: unknown) => { - const objHandle = wrapIfHandle(objData) as EnvironmentCallbackContextHandle; - const obj = new EnvironmentCallbackContext(objHandle, this._client); - await callback(obj); + private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; + const arg = new EnvironmentCallbackContext(argHandle, this._client); + await callback(arg); }); const rpcArgs: Record = { builder: this._handle, callback: callbackId }; const result = await this._client.invokeCapability( @@ -18257,43 +18016,38 @@ export class TestDatabaseResource extends ResourceBuilderBase Promise): TestDatabaseResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestDatabaseResourcePromise { return new TestDatabaseResourcePromise(this._withEnvironmentCallbackInternal(callback)); } /** @internal */ - private async _withEnvironmentCallbackAsyncInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (argData: unknown) => { - const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; - const arg = new EnvironmentCallbackContext(argHandle, this._client); - await callback(arg); - }); - const rpcArgs: Record = { builder: this._handle, callback: callbackId }; + private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { + const rpcArgs: Record = { builder: this._handle, name, endpointReference }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentCallbackAsync', + 'Aspire.Hosting/withEnvironmentEndpoint', rpcArgs ); return new TestDatabaseResource(result, this._client); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromise(this._withEnvironmentCallbackAsyncInternal(callback)); + /** Sets an environment variable from an endpoint reference */ + withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); } /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; + private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): Promise { + const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', + 'Aspire.Hosting/withEnvironment', rpcArgs ); return new TestDatabaseResource(result, this._client); } - /** Sets an environment variable from an endpoint reference */ - withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromise(this._withEnvironmentInternal(name, value)); } /** @internal */ @@ -18699,7 +18453,7 @@ export class TestDatabaseResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitFor', + 'Aspire.Hosting/waitForResource', rpcArgs ); return new TestDatabaseResource(result, this._client); @@ -18729,7 +18483,7 @@ export class TestDatabaseResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForStart', + 'Aspire.Hosting/waitForResourceStart', rpcArgs ); return new TestDatabaseResource(result, this._client); @@ -18775,7 +18529,7 @@ export class TestDatabaseResource extends ResourceBuilderBase = { builder: this._handle, dependency }; if (exitCode !== undefined) rpcArgs.exitCode = exitCode; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForCompletion', + 'Aspire.Hosting/waitForResourceCompletion', rpcArgs ); return new TestDatabaseResource(result, this._client); @@ -18880,7 +18634,7 @@ export class TestDatabaseResource extends ResourceBuilderBase = { builder: this._handle }; if (password !== undefined) rpcArgs.password = password; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withHttpsDeveloperCertificate', + 'Aspire.Hosting/withParameterHttpsDeveloperCertificate', rpcArgs ); return new TestDatabaseResource(result, this._client); @@ -18911,7 +18665,7 @@ export class TestDatabaseResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new TestDatabaseResource(result, this._client); @@ -18926,7 +18680,7 @@ export class TestDatabaseResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new TestDatabaseResource(result, this._client); @@ -19606,31 +19360,21 @@ export class TestDatabaseResourcePromise implements PromiseLike obj.withRequiredCommand(command, options))); } - /** Sets an environment variable */ - withEnvironment(name: string, value: string): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromise(this._promise.then(obj => obj.withEnvironmentExpression(name, value))); - } - /** Sets environment variables via callback */ - withEnvironmentCallback(callback: (obj: EnvironmentCallbackContext) => Promise): TestDatabaseResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestDatabaseResourcePromise { return new TestDatabaseResourcePromise(this._promise.then(obj => obj.withEnvironmentCallback(callback))); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): TestDatabaseResourcePromise { - return new TestDatabaseResourcePromise(this._promise.then(obj => obj.withEnvironmentCallbackAsync(callback))); - } - /** Sets an environment variable from an endpoint reference */ withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): TestDatabaseResourcePromise { return new TestDatabaseResourcePromise(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference))); } + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): TestDatabaseResourcePromise { + return new TestDatabaseResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); + } + /** Sets an environment variable from a parameter resource */ withEnvironmentParameter(name: string, parameter: ParameterResource): TestDatabaseResourcePromise { return new TestDatabaseResourcePromise(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter))); @@ -20184,7 +19928,7 @@ export class TestRedisResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBuildArg', + 'Aspire.Hosting/withParameterBuildArg', rpcArgs ); return new TestRedisResource(result, this._client); @@ -20199,7 +19943,7 @@ export class TestRedisResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBuildSecret', + 'Aspire.Hosting/withParameterBuildSecret', rpcArgs ); return new TestRedisResource(result, this._client); @@ -20341,41 +20085,11 @@ export class TestRedisResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironment', - rpcArgs - ); - return new TestRedisResource(result, this._client); - } - - /** Sets an environment variable */ - withEnvironment(name: string, value: string): TestRedisResourcePromise { - return new TestRedisResourcePromise(this._withEnvironmentInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new TestRedisResource(result, this._client); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestRedisResourcePromise { - return new TestRedisResourcePromise(this._withEnvironmentExpressionInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentCallbackInternal(callback: (obj: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (objData: unknown) => { - const objHandle = wrapIfHandle(objData) as EnvironmentCallbackContextHandle; - const obj = new EnvironmentCallbackContext(objHandle, this._client); - await callback(obj); + private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; + const arg = new EnvironmentCallbackContext(argHandle, this._client); + await callback(arg); }); const rpcArgs: Record = { builder: this._handle, callback: callbackId }; const result = await this._client.invokeCapability( @@ -20386,43 +20100,38 @@ export class TestRedisResource extends ResourceBuilderBase Promise): TestRedisResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestRedisResourcePromise { return new TestRedisResourcePromise(this._withEnvironmentCallbackInternal(callback)); } /** @internal */ - private async _withEnvironmentCallbackAsyncInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (argData: unknown) => { - const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; - const arg = new EnvironmentCallbackContext(argHandle, this._client); - await callback(arg); - }); - const rpcArgs: Record = { builder: this._handle, callback: callbackId }; + private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { + const rpcArgs: Record = { builder: this._handle, name, endpointReference }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentCallbackAsync', + 'Aspire.Hosting/withEnvironmentEndpoint', rpcArgs ); return new TestRedisResource(result, this._client); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): TestRedisResourcePromise { - return new TestRedisResourcePromise(this._withEnvironmentCallbackAsyncInternal(callback)); + /** Sets an environment variable from an endpoint reference */ + withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): TestRedisResourcePromise { + return new TestRedisResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); } /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; + private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): Promise { + const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', + 'Aspire.Hosting/withEnvironment', rpcArgs ); return new TestRedisResource(result, this._client); } - /** Sets an environment variable from an endpoint reference */ - withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): TestRedisResourcePromise { - return new TestRedisResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): TestRedisResourcePromise { + return new TestRedisResourcePromise(this._withEnvironmentInternal(name, value)); } /** @internal */ @@ -20561,6 +20270,15 @@ export class TestRedisResource extends ResourceBuilderBase { + const rpcArgs: Record = { resource: this._handle, key }; + return await this._client.invokeCapability( + 'Aspire.Hosting/getConnectionProperty', + rpcArgs + ); + } + /** @internal */ private async _withReferenceUriInternal(name: string, uri: string): Promise { const rpcArgs: Record = { builder: this._handle, name, uri }; @@ -20858,7 +20576,7 @@ export class TestRedisResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitFor', + 'Aspire.Hosting/waitForResource', rpcArgs ); return new TestRedisResource(result, this._client); @@ -20888,7 +20606,7 @@ export class TestRedisResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForStart', + 'Aspire.Hosting/waitForResourceStart', rpcArgs ); return new TestRedisResource(result, this._client); @@ -20934,7 +20652,7 @@ export class TestRedisResource extends ResourceBuilderBase = { builder: this._handle, dependency }; if (exitCode !== undefined) rpcArgs.exitCode = exitCode; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForCompletion', + 'Aspire.Hosting/waitForResourceCompletion', rpcArgs ); return new TestRedisResource(result, this._client); @@ -21039,7 +20757,7 @@ export class TestRedisResource extends ResourceBuilderBase = { builder: this._handle }; if (password !== undefined) rpcArgs.password = password; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withHttpsDeveloperCertificate', + 'Aspire.Hosting/withParameterHttpsDeveloperCertificate', rpcArgs ); return new TestRedisResource(result, this._client); @@ -21070,7 +20788,7 @@ export class TestRedisResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new TestRedisResource(result, this._client); @@ -21085,7 +20803,7 @@ export class TestRedisResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new TestRedisResource(result, this._client); @@ -21954,31 +21672,21 @@ export class TestRedisResourcePromise implements PromiseLike return new TestRedisResourcePromise(this._promise.then(obj => obj.withRequiredCommand(command, options))); } - /** Sets an environment variable */ - withEnvironment(name: string, value: string): TestRedisResourcePromise { - return new TestRedisResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestRedisResourcePromise { - return new TestRedisResourcePromise(this._promise.then(obj => obj.withEnvironmentExpression(name, value))); - } - /** Sets environment variables via callback */ - withEnvironmentCallback(callback: (obj: EnvironmentCallbackContext) => Promise): TestRedisResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestRedisResourcePromise { return new TestRedisResourcePromise(this._promise.then(obj => obj.withEnvironmentCallback(callback))); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): TestRedisResourcePromise { - return new TestRedisResourcePromise(this._promise.then(obj => obj.withEnvironmentCallbackAsync(callback))); - } - /** Sets an environment variable from an endpoint reference */ withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): TestRedisResourcePromise { return new TestRedisResourcePromise(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference))); } + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): TestRedisResourcePromise { + return new TestRedisResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); + } + /** Sets an environment variable from a parameter resource */ withEnvironmentParameter(name: string, parameter: ParameterResource): TestRedisResourcePromise { return new TestRedisResourcePromise(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter))); @@ -22019,6 +21727,11 @@ export class TestRedisResourcePromise implements PromiseLike return new TestRedisResourcePromise(this._promise.then(obj => obj.withReference(source, options))); } + /** Gets a connection property by key */ + getConnectionProperty(key: string): Promise { + return this._promise.then(obj => obj.getConnectionProperty(key)); + } + /** Adds a reference to a URI */ withReferenceUri(name: string, uri: string): TestRedisResourcePromise { return new TestRedisResourcePromise(this._promise.then(obj => obj.withReferenceUri(name, uri))); @@ -22607,7 +22320,7 @@ export class TestVaultResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBuildArg', + 'Aspire.Hosting/withParameterBuildArg', rpcArgs ); return new TestVaultResource(result, this._client); @@ -22622,7 +22335,7 @@ export class TestVaultResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withBuildSecret', + 'Aspire.Hosting/withParameterBuildSecret', rpcArgs ); return new TestVaultResource(result, this._client); @@ -22764,41 +22477,11 @@ export class TestVaultResource extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironment', - rpcArgs - ); - return new TestVaultResource(result, this._client); - } - - /** Sets an environment variable */ - withEnvironment(name: string, value: string): TestVaultResourcePromise { - return new TestVaultResourcePromise(this._withEnvironmentInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new TestVaultResource(result, this._client); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestVaultResourcePromise { - return new TestVaultResourcePromise(this._withEnvironmentExpressionInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentCallbackInternal(callback: (obj: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (objData: unknown) => { - const objHandle = wrapIfHandle(objData) as EnvironmentCallbackContextHandle; - const obj = new EnvironmentCallbackContext(objHandle, this._client); - await callback(obj); + private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; + const arg = new EnvironmentCallbackContext(argHandle, this._client); + await callback(arg); }); const rpcArgs: Record = { builder: this._handle, callback: callbackId }; const result = await this._client.invokeCapability( @@ -22809,43 +22492,38 @@ export class TestVaultResource extends ResourceBuilderBase Promise): TestVaultResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestVaultResourcePromise { return new TestVaultResourcePromise(this._withEnvironmentCallbackInternal(callback)); } /** @internal */ - private async _withEnvironmentCallbackAsyncInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (argData: unknown) => { - const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; - const arg = new EnvironmentCallbackContext(argHandle, this._client); - await callback(arg); - }); - const rpcArgs: Record = { builder: this._handle, callback: callbackId }; + private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { + const rpcArgs: Record = { builder: this._handle, name, endpointReference }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentCallbackAsync', + 'Aspire.Hosting/withEnvironmentEndpoint', rpcArgs ); return new TestVaultResource(result, this._client); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): TestVaultResourcePromise { - return new TestVaultResourcePromise(this._withEnvironmentCallbackAsyncInternal(callback)); + /** Sets an environment variable from an endpoint reference */ + withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): TestVaultResourcePromise { + return new TestVaultResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); } /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; + private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): Promise { + const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', + 'Aspire.Hosting/withEnvironment', rpcArgs ); return new TestVaultResource(result, this._client); } - /** Sets an environment variable from an endpoint reference */ - withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): TestVaultResourcePromise { - return new TestVaultResourcePromise(this._withEnvironmentEndpointInternal(name, endpointReference)); + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): TestVaultResourcePromise { + return new TestVaultResourcePromise(this._withEnvironmentInternal(name, value)); } /** @internal */ @@ -23251,7 +22929,7 @@ export class TestVaultResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitFor', + 'Aspire.Hosting/waitForResource', rpcArgs ); return new TestVaultResource(result, this._client); @@ -23281,7 +22959,7 @@ export class TestVaultResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForStart', + 'Aspire.Hosting/waitForResourceStart', rpcArgs ); return new TestVaultResource(result, this._client); @@ -23327,7 +23005,7 @@ export class TestVaultResource extends ResourceBuilderBase = { builder: this._handle, dependency }; if (exitCode !== undefined) rpcArgs.exitCode = exitCode; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForCompletion', + 'Aspire.Hosting/waitForResourceCompletion', rpcArgs ); return new TestVaultResource(result, this._client); @@ -23432,7 +23110,7 @@ export class TestVaultResource extends ResourceBuilderBase = { builder: this._handle }; if (password !== undefined) rpcArgs.password = password; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withHttpsDeveloperCertificate', + 'Aspire.Hosting/withParameterHttpsDeveloperCertificate', rpcArgs ); return new TestVaultResource(result, this._client); @@ -23463,7 +23141,7 @@ export class TestVaultResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new TestVaultResource(result, this._client); @@ -23478,7 +23156,7 @@ export class TestVaultResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new TestVaultResource(result, this._client); @@ -24173,31 +23851,21 @@ export class TestVaultResourcePromise implements PromiseLike return new TestVaultResourcePromise(this._promise.then(obj => obj.withRequiredCommand(command, options))); } - /** Sets an environment variable */ - withEnvironment(name: string, value: string): TestVaultResourcePromise { - return new TestVaultResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): TestVaultResourcePromise { - return new TestVaultResourcePromise(this._promise.then(obj => obj.withEnvironmentExpression(name, value))); - } - /** Sets environment variables via callback */ - withEnvironmentCallback(callback: (obj: EnvironmentCallbackContext) => Promise): TestVaultResourcePromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): TestVaultResourcePromise { return new TestVaultResourcePromise(this._promise.then(obj => obj.withEnvironmentCallback(callback))); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): TestVaultResourcePromise { - return new TestVaultResourcePromise(this._promise.then(obj => obj.withEnvironmentCallbackAsync(callback))); - } - /** Sets an environment variable from an endpoint reference */ withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): TestVaultResourcePromise { return new TestVaultResourcePromise(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference))); } + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): TestVaultResourcePromise { + return new TestVaultResourcePromise(this._promise.then(obj => obj.withEnvironment(name, value))); + } + /** Sets an environment variable from a parameter resource */ withEnvironmentParameter(name: string, parameter: ParameterResource): TestVaultResourcePromise { return new TestVaultResourcePromise(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter))); @@ -24621,7 +24289,7 @@ export class ContainerFilesDestinationResource extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, source, destinationPath }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/publishWithContainerFiles', + 'Aspire.Hosting/publishWithContainerFilesFromResource', rpcArgs ); return new ContainerFilesDestinationResource(result, this._client); @@ -24880,7 +24548,7 @@ export class Resource extends ResourceBuilderBase { private async _withParentRelationshipInternal(parent: ResourceBuilderBase): Promise { const rpcArgs: Record = { builder: this._handle, parent }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withParentRelationship', + 'Aspire.Hosting/withBuilderParentRelationship', rpcArgs ); return new Resource(result, this._client); @@ -24895,7 +24563,7 @@ export class Resource extends ResourceBuilderBase { private async _withChildRelationshipInternal(child: ResourceBuilderBase): Promise { const rpcArgs: Record = { builder: this._handle, child }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withChildRelationship', + 'Aspire.Hosting/withBuilderChildRelationship', rpcArgs ); return new Resource(result, this._client); @@ -25651,6 +25319,15 @@ export class ResourceWithConnectionString extends ResourceBuilderBase { + const rpcArgs: Record = { resource: this._handle, key }; + return await this._client.invokeCapability( + 'Aspire.Hosting/getConnectionProperty', + rpcArgs + ); + } + /** @internal */ private async _onConnectionStringAvailableInternal(callback: (arg: ConnectionStringAvailableEvent) => Promise): Promise { const callbackId = registerCallback(async (argData: unknown) => { @@ -25728,6 +25405,11 @@ export class ResourceWithConnectionStringPromise implements PromiseLike obj.withConnectionPropertyValue(name, value))); } + /** Gets a connection property by key */ + getConnectionProperty(key: string): Promise { + return this._promise.then(obj => obj.getConnectionProperty(key)); + } + /** Subscribes to the ConnectionStringAvailable event */ onConnectionStringAvailable(callback: (arg: ConnectionStringAvailableEvent) => Promise): ResourceWithConnectionStringPromise { return new ResourceWithConnectionStringPromise(this._promise.then(obj => obj.onConnectionStringAvailable(callback))); @@ -26165,41 +25847,11 @@ export class ResourceWithEnvironment extends ResourceBuilderBase { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironment', - rpcArgs - ); - return new ResourceWithEnvironment(result, this._client); - } - - /** Sets an environment variable */ - withEnvironment(name: string, value: string): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromise(this._withEnvironmentInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentExpressionInternal(name: string, value: ReferenceExpression): Promise { - const rpcArgs: Record = { builder: this._handle, name, value }; - const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentExpression', - rpcArgs - ); - return new ResourceWithEnvironment(result, this._client); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromise(this._withEnvironmentExpressionInternal(name, value)); - } - - /** @internal */ - private async _withEnvironmentCallbackInternal(callback: (obj: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (objData: unknown) => { - const objHandle = wrapIfHandle(objData) as EnvironmentCallbackContextHandle; - const obj = new EnvironmentCallbackContext(objHandle, this._client); - await callback(obj); + private async _withEnvironmentCallbackInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { + const callbackId = registerCallback(async (argData: unknown) => { + const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; + const arg = new EnvironmentCallbackContext(argHandle, this._client); + await callback(arg); }); const rpcArgs: Record = { builder: this._handle, callback: callbackId }; const result = await this._client.invokeCapability( @@ -26210,43 +25862,38 @@ export class ResourceWithEnvironment extends ResourceBuilderBase Promise): ResourceWithEnvironmentPromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ResourceWithEnvironmentPromise { return new ResourceWithEnvironmentPromise(this._withEnvironmentCallbackInternal(callback)); } /** @internal */ - private async _withEnvironmentCallbackAsyncInternal(callback: (arg: EnvironmentCallbackContext) => Promise): Promise { - const callbackId = registerCallback(async (argData: unknown) => { - const argHandle = wrapIfHandle(argData) as EnvironmentCallbackContextHandle; - const arg = new EnvironmentCallbackContext(argHandle, this._client); - await callback(arg); - }); - const rpcArgs: Record = { builder: this._handle, callback: callbackId }; + private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { + const rpcArgs: Record = { builder: this._handle, name, endpointReference }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentCallbackAsync', + 'Aspire.Hosting/withEnvironmentEndpoint', rpcArgs ); return new ResourceWithEnvironment(result, this._client); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromise(this._withEnvironmentCallbackAsyncInternal(callback)); + /** Sets an environment variable from an endpoint reference */ + withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ResourceWithEnvironmentPromise { + return new ResourceWithEnvironmentPromise(this._withEnvironmentEndpointInternal(name, endpointReference)); } /** @internal */ - private async _withEnvironmentEndpointInternal(name: string, endpointReference: EndpointReference): Promise { - const rpcArgs: Record = { builder: this._handle, name, endpointReference }; + private async _withEnvironmentInternal(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): Promise { + const rpcArgs: Record = { builder: this._handle, name, value }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withEnvironmentEndpoint', + 'Aspire.Hosting/withEnvironment', rpcArgs ); return new ResourceWithEnvironment(result, this._client); } - /** Sets an environment variable from an endpoint reference */ - withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromise(this._withEnvironmentEndpointInternal(name, endpointReference)); + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): ResourceWithEnvironmentPromise { + return new ResourceWithEnvironmentPromise(this._withEnvironmentInternal(name, value)); } /** @internal */ @@ -26380,7 +26027,7 @@ export class ResourceWithEnvironment extends ResourceBuilderBase = { builder: this._handle }; if (password !== undefined) rpcArgs.password = password; const result = await this._client.invokeCapability( - 'Aspire.Hosting/withHttpsDeveloperCertificate', + 'Aspire.Hosting/withParameterHttpsDeveloperCertificate', rpcArgs ); return new ResourceWithEnvironment(result, this._client); @@ -26469,31 +26116,21 @@ export class ResourceWithEnvironmentPromise implements PromiseLike obj.withOtlpExporterProtocol(protocol))); } - /** Sets an environment variable */ - withEnvironment(name: string, value: string): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromise(this._promise.then(obj => obj.withEnvironment(name, value))); - } - - /** Adds an environment variable with a reference expression */ - withEnvironmentExpression(name: string, value: ReferenceExpression): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromise(this._promise.then(obj => obj.withEnvironmentExpression(name, value))); - } - /** Sets environment variables via callback */ - withEnvironmentCallback(callback: (obj: EnvironmentCallbackContext) => Promise): ResourceWithEnvironmentPromise { + withEnvironmentCallback(callback: (arg: EnvironmentCallbackContext) => Promise): ResourceWithEnvironmentPromise { return new ResourceWithEnvironmentPromise(this._promise.then(obj => obj.withEnvironmentCallback(callback))); } - /** Sets environment variables via async callback */ - withEnvironmentCallbackAsync(callback: (arg: EnvironmentCallbackContext) => Promise): ResourceWithEnvironmentPromise { - return new ResourceWithEnvironmentPromise(this._promise.then(obj => obj.withEnvironmentCallbackAsync(callback))); - } - /** Sets an environment variable from an endpoint reference */ withEnvironmentEndpoint(name: string, endpointReference: EndpointReference): ResourceWithEnvironmentPromise { return new ResourceWithEnvironmentPromise(this._promise.then(obj => obj.withEnvironmentEndpoint(name, endpointReference))); } + /** Sets an environment variable on the resource */ + withEnvironment(name: string, value: string | ReferenceExpression | EndpointReference | ParameterResource | ResourceWithConnectionString): ResourceWithEnvironmentPromise { + return new ResourceWithEnvironmentPromise(this._promise.then(obj => obj.withEnvironment(name, value))); + } + /** Sets an environment variable from a parameter resource */ withEnvironmentParameter(name: string, parameter: ParameterResource): ResourceWithEnvironmentPromise { return new ResourceWithEnvironmentPromise(this._promise.then(obj => obj.withEnvironmentParameter(name, parameter))); @@ -26569,7 +26206,7 @@ export class ResourceWithWaitSupport extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitFor', + 'Aspire.Hosting/waitForResource', rpcArgs ); return new ResourceWithWaitSupport(result, this._client); @@ -26599,7 +26236,7 @@ export class ResourceWithWaitSupport extends ResourceBuilderBase { const rpcArgs: Record = { builder: this._handle, dependency }; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForStart', + 'Aspire.Hosting/waitForResourceStart', rpcArgs ); return new ResourceWithWaitSupport(result, this._client); @@ -26630,7 +26267,7 @@ export class ResourceWithWaitSupport extends ResourceBuilderBase = { builder: this._handle, dependency }; if (exitCode !== undefined) rpcArgs.exitCode = exitCode; const result = await this._client.invokeCapability( - 'Aspire.Hosting/waitForCompletion', + 'Aspire.Hosting/waitForResourceCompletion', rpcArgs ); return new ResourceWithWaitSupport(result, this._client); diff --git a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/transport.verified.ts b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/transport.verified.ts index 3df2a47d497..6d29cf289d9 100644 --- a/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/transport.verified.ts +++ b/tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/transport.verified.ts @@ -1,4 +1,4 @@ -// transport.ts - ATS transport layer: RPC, Handle, errors, callbacks +// transport.ts - ATS transport layer: RPC, Handle, errors, callbacks import * as net from 'net'; import * as rpc from 'vscode-jsonrpc/node.js'; @@ -830,7 +830,7 @@ export class AspireClient { failConnect(new Error('Connection closed before JSON-RPC was established')); }; - const onConnect = () => { + const onConnect = async () => { if (settled) { return; } @@ -867,7 +867,16 @@ export class AspireClient { socket.on('error', onConnectedSocketError); socket.on('close', onConnectedSocketClose); + const authToken = process.env.ASPIRE_REMOTE_APPHOST_TOKEN; + if (!authToken) { + throw new Error('ASPIRE_REMOTE_APPHOST_TOKEN environment variable is not set.'); + } this.connection.listen(); + const authenticated = await this.connection.sendRequest('authenticate', authToken); + if (!authenticated) { + throw new Error('Failed to authenticate to the AppHost server.'); + } + connectedClients.add(this); this._connectPromise = null; settled = true; diff --git a/tests/Aspire.Hosting.Foundry.Tests/AddProjectTests.cs b/tests/Aspire.Hosting.Foundry.Tests/AddProjectTests.cs index 639b7f5f9b9..93c80aab616 100644 --- a/tests/Aspire.Hosting.Foundry.Tests/AddProjectTests.cs +++ b/tests/Aspire.Hosting.Foundry.Tests/AddProjectTests.cs @@ -43,4 +43,30 @@ public async Task AddProject_WithReference_ShouldBindUriConnectionProperty() && value is "{test-project.outputs.endpoint}"; }); } + + [Fact] + public void AddProject_AfterRunAsFoundryLocal_Throws() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); + + var foundry = builder.AddFoundry("account") + .RunAsFoundryLocal(); + + var exception = Assert.Throws(() => foundry.AddProject("my-project")); + + Assert.Equal(FoundryExtensions.LocalProjectsNotSupportedMessage, exception.Message); + } + + [Fact] + public void RunAsFoundryLocal_AfterAddProject_Throws() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); + + var foundry = builder.AddFoundry("account"); + foundry.AddProject("my-project"); + + var exception = Assert.Throws(foundry.RunAsFoundryLocal); + + Assert.Equal(FoundryExtensions.LocalProjectsNotSupportedMessage, exception.Message); + } } diff --git a/tests/Aspire.Hosting.Foundry.Tests/ProjectResourceTests.cs b/tests/Aspire.Hosting.Foundry.Tests/ProjectResourceTests.cs index 6b12b3b4940..e83afad02f7 100644 --- a/tests/Aspire.Hosting.Foundry.Tests/ProjectResourceTests.cs +++ b/tests/Aspire.Hosting.Foundry.Tests/ProjectResourceTests.cs @@ -42,7 +42,9 @@ public void AddProject_InRunMode_ModelsDefaultContainerRegistry() var project = builder.AddFoundry("account") .AddProject("my-project"); - Assert.Empty(builder.Resources.OfType()); + var registry = Assert.Single(builder.Resources.OfType()); + Assert.Equal("my-project-acr", registry.Name); + Assert.Same(project.Resource.DefaultContainerRegistry, registry); Assert.Same(project.Resource.DefaultContainerRegistry, project.Resource.ContainerRegistry); } diff --git a/tests/Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.csproj b/tests/Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.csproj index 4e7ba3f8a1a..6f3d4eac806 100644 --- a/tests/Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.csproj +++ b/tests/Aspire.Hosting.RemoteHost.Tests/Aspire.Hosting.RemoteHost.Tests.csproj @@ -6,6 +6,11 @@ + + + + + diff --git a/tests/Aspire.Hosting.RemoteHost.Tests/AttributeDataReaderTests.cs b/tests/Aspire.Hosting.RemoteHost.Tests/AttributeDataReaderTests.cs index faf7a0bd9e8..b7565415944 100644 --- a/tests/Aspire.Hosting.RemoteHost.Tests/AttributeDataReaderTests.cs +++ b/tests/Aspire.Hosting.RemoteHost.Tests/AttributeDataReaderTests.cs @@ -69,6 +69,14 @@ public void HasAspireExportIgnoreData_IgnoresAttribute_WithDifferentNamespace() Assert.False(result); } + [Fact] + public void HasAspireExportIgnoreData_FindsOfficialAttribute_OnType() + { + var result = AttributeDataReader.HasAspireExportIgnoreData(typeof(OfficialIgnoredType)); + + Assert.True(result); + } + [Fact] public void HasAspireDtoData_FindsOfficialAttribute() { @@ -216,6 +224,11 @@ public OfficialMethodsResource(string name) : base(name) { } public void DoSomething() { } } + [AspireExportIgnore] + public class OfficialIgnoredType + { + } + [AspireDto] public class OfficialDtoType { diff --git a/tests/Aspire.Hosting.RemoteHost.Tests/CodeGenerationResolverTests.cs b/tests/Aspire.Hosting.RemoteHost.Tests/CodeGenerationResolverTests.cs new file mode 100644 index 00000000000..adc98f38d47 --- /dev/null +++ b/tests/Aspire.Hosting.RemoteHost.Tests/CodeGenerationResolverTests.cs @@ -0,0 +1,60 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.RemoteHost.CodeGeneration; +using Aspire.Hosting.RemoteHost.Language; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Xunit; + +namespace Aspire.Hosting.RemoteHost.Tests; + +public class CodeGenerationResolverTests +{ + [Fact] + public void CodeGeneratorResolver_DiscoversInternalCodeGenerators() + { + using var serviceProvider = CreateServiceProvider(); + var assemblyLoader = CreateAssemblyLoader(); + var resolver = new CodeGeneratorResolver(serviceProvider, assemblyLoader, NullLogger.Instance); + + Assert.NotNull(resolver.GetCodeGenerator("Go")); + Assert.NotNull(resolver.GetCodeGenerator("Java")); + Assert.NotNull(resolver.GetCodeGenerator("Python")); + Assert.NotNull(resolver.GetCodeGenerator("Rust")); + Assert.NotNull(resolver.GetCodeGenerator("TypeScript")); + } + + [Fact] + public void LanguageSupportResolver_DiscoversInternalLanguageSupports() + { + using var serviceProvider = CreateServiceProvider(); + var assemblyLoader = CreateAssemblyLoader(); + var resolver = new LanguageSupportResolver(serviceProvider, assemblyLoader, NullLogger.Instance); + + Assert.NotNull(resolver.GetLanguageSupport("go")); + Assert.NotNull(resolver.GetLanguageSupport("java")); + Assert.NotNull(resolver.GetLanguageSupport("python")); + Assert.NotNull(resolver.GetLanguageSupport("rust")); + Assert.NotNull(resolver.GetLanguageSupport("typescript/nodejs")); + } + + private static ServiceProvider CreateServiceProvider() => new ServiceCollection().BuildServiceProvider(); + + private static AssemblyLoader CreateAssemblyLoader() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["AtsAssemblies:0"] = "Aspire.Hosting.CodeGeneration.Go", + ["AtsAssemblies:1"] = "Aspire.Hosting.CodeGeneration.Java", + ["AtsAssemblies:2"] = "Aspire.Hosting.CodeGeneration.Python", + ["AtsAssemblies:3"] = "Aspire.Hosting.CodeGeneration.Rust", + ["AtsAssemblies:4"] = "Aspire.Hosting.CodeGeneration.TypeScript", + }) + .Build(); + + return new AssemblyLoader(configuration, NullLogger.Instance); + } +} diff --git a/tests/Aspire.Hosting.RemoteHost.Tests/JsonRpcAuthenticationTests.cs b/tests/Aspire.Hosting.RemoteHost.Tests/JsonRpcAuthenticationTests.cs new file mode 100644 index 00000000000..789d0c18652 --- /dev/null +++ b/tests/Aspire.Hosting.RemoteHost.Tests/JsonRpcAuthenticationTests.cs @@ -0,0 +1,244 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.IO.Pipes; +using System.Net.Sockets; +using System.Text.Json; +using Aspire.Hosting.RemoteHost.Ats; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using StreamJsonRpc; +using Xunit; + +namespace Aspire.Hosting.RemoteHost.Tests; + +public sealed class JsonRpcAuthenticationTests +{ + public static TheoryData ProtectedMethods => new() + { + { "cancelToken", ["ct_missing"] }, + { "invokeCapability", ["test-capability", null] }, + { "getCapabilities", [] }, + { "generateCode", ["TypeScript"] }, + { "scaffoldAppHost", ["TypeScript", "/tmp/apphost", "AppHost"] }, + { "detectAppHostType", ["/tmp/apphost"] }, + { "getRuntimeSpec", ["TypeScript"] } + }; + + [Fact] + public async Task Ping_DoesNotRequireAuthentication() + { + await using var server = await RemoteHostTestServer.StartAsync(); + await using var client = await server.ConnectAsync(); + + var result = await client.InvokeAsync("ping"); + + Assert.Equal("pong", result); + } + + [Theory] + [MemberData(nameof(ProtectedMethods))] + public async Task ProtectedMethods_RequireAuthentication(string methodName, object?[] arguments) + { + await using var server = await RemoteHostTestServer.StartAsync(); + await using var client = await server.ConnectAsync(); + + var ex = await Assert.ThrowsAsync( + () => client.InvokeAsync(methodName, arguments)); + + Assert.Contains("Client must authenticate before invoking AppHost RPC methods.", ex.Message); + } + + [Fact] + public async Task FailedAuthentication_ClosesConnection_AndPreventsFurtherCalls() + { + await using var server = await RemoteHostTestServer.StartAsync(); + await using var client = await server.ConnectAsync(); + + Assert.Equal("pong", await client.InvokeAsync("ping")); + + await AssertRejectedAuthenticationAsync(client); + await RemoteHostTestServer.WaitForDisconnectAsync(client); + + await Assert.ThrowsAnyAsync(() => client.InvokeAsync("ping")); + await Assert.ThrowsAnyAsync(() => client.InvokeAsync("cancelToken", ["ct_missing"])); + } + + private static async Task AssertRejectedAuthenticationAsync(JsonRpcClientHandle client) + { + try + { + var authenticated = await client.InvokeAsync("authenticate", ["wrong-token"]); + Assert.False(authenticated); + } + catch (ConnectionLostException) + { + // The server closes the connection immediately after rejecting the token, so the client may observe + // the disconnect before it receives the boolean response. + } + } + + private sealed class RemoteHostTestServer : IAsyncDisposable + { + private const string RemoteAppHostToken = "ASPIRE_REMOTE_APPHOST_TOKEN"; + private readonly IHost _host; + private readonly string _socketPath; + private readonly string? _socketDirectory; + + private RemoteHostTestServer(IHost host, string socketPath, string? socketDirectory) + { + _host = host; + _socketPath = socketPath; + _socketDirectory = socketDirectory; + } + + public static async Task StartAsync() + { + var socketDirectory = OperatingSystem.IsWindows() + ? null + : Path.Combine(Path.GetTempPath(), $"arh-{Guid.NewGuid():N}"[..12]); + + if (socketDirectory is not null) + { + Directory.CreateDirectory(socketDirectory); + } + + var socketPath = OperatingSystem.IsWindows() + ? $"aspire-remotehost-test-{Guid.NewGuid():N}" + : Path.Combine(socketDirectory!, "rpc.sock"); + + var builder = Host.CreateApplicationBuilder(); + builder.Configuration.AddInMemoryCollection(new Dictionary + { + ["REMOTE_APP_HOST_SOCKET_PATH"] = socketPath, + [RemoteAppHostToken] = "expected-token" + }); + + ConfigureServices(builder.Services); + + var host = builder.Build(); + await host.StartAsync(); + + return new RemoteHostTestServer(host, socketPath, socketDirectory); + } + + public async Task ConnectAsync() + { + var stream = await ConnectToServerAsync(_socketPath, CancellationToken.None); + var formatter = new SystemTextJsonFormatter(); + var handler = new HeaderDelimitedMessageHandler(stream, stream, formatter); + var rpc = new JsonRpc(handler); + rpc.StartListening(); + + return new JsonRpcClientHandle(stream, rpc); + } + + public static async Task WaitForDisconnectAsync(JsonRpcClientHandle client) + { + var completedTask = await Task.WhenAny(client.Completion, Task.Delay(TimeSpan.FromSeconds(5))); + Assert.Same(client.Completion, completedTask); + } + + public async ValueTask DisposeAsync() + { + await _host.StopAsync(); + _host.Dispose(); + + if (!OperatingSystem.IsWindows() && File.Exists(_socketPath)) + { + File.Delete(_socketPath); + } + + if (!string.IsNullOrEmpty(_socketDirectory) && Directory.Exists(_socketDirectory)) + { + Directory.Delete(_socketDirectory, recursive: true); + } + } + + private static void ConfigureServices(IServiceCollection services) + { + services.AddHostedService(); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(sp => sp.GetRequiredService().GetContext()); + services.AddSingleton(); + services.AddScoped(); + services.AddSingleton(); + services.AddScoped(); + + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(sp => sp.GetRequiredService()); + services.AddScoped(); + services.AddScoped(sp => new Lazy(() => sp.GetRequiredService())); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + } + + private static async Task ConnectToServerAsync(string socketPath, CancellationToken cancellationToken) + { + using var timeoutCts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); + using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, timeoutCts.Token); + + Exception? lastException = null; + + while (!linkedCts.Token.IsCancellationRequested) + { + try + { + if (OperatingSystem.IsWindows()) + { + var pipeClient = new NamedPipeClientStream(".", socketPath, PipeDirection.InOut, PipeOptions.Asynchronous); + await pipeClient.ConnectAsync(linkedCts.Token); + return pipeClient; + } + + var socket = new Socket(AddressFamily.Unix, SocketType.Stream, ProtocolType.Unspecified); + await socket.ConnectAsync(new UnixDomainSocketEndPoint(socketPath), linkedCts.Token); + return new NetworkStream(socket, ownsSocket: true); + } + catch (Exception ex) when (ex is IOException or SocketException or TimeoutException or OperationCanceledException) + { + lastException = ex; + + if (timeoutCts.IsCancellationRequested) + { + break; + } + + await Task.Delay(100, cancellationToken); + } + } + + throw new TimeoutException($"Timed out connecting to test RPC server '{socketPath}'.", lastException); + } + } + + private sealed class JsonRpcClientHandle : IAsyncDisposable + { + private readonly Stream _stream; + private readonly JsonRpc _rpc; + + public JsonRpcClientHandle(Stream stream, JsonRpc rpc) + { + _stream = stream; + _rpc = rpc; + } + + public Task Completion => _rpc.Completion; + + public Task InvokeAsync(string methodName, params object?[] arguments) + => _rpc.InvokeWithCancellationAsync(methodName, arguments, CancellationToken.None); + + public async ValueTask DisposeAsync() + { + _rpc.Dispose(); + await _stream.DisposeAsync(); + } + } +} diff --git a/tests/helix/send-to-helix-inner.proj b/tests/helix/send-to-helix-inner.proj index 4601ddb606a..b8ab64d31c5 100644 --- a/tests/helix/send-to-helix-inner.proj +++ b/tests/helix/send-to-helix-inner.proj @@ -10,7 +10,7 @@ sdk true true - + true $(DotNetSdkPreviousVersionForTesting) @@ -144,7 +144,7 @@ - +