Skip to content

fix: switch to foundry backend urls - #9334

Open
farhann1 wants to merge 7 commits into
mainfrom
farhannawaz/rle-control-plane
Open

fix: switch to foundry backend urls#9334
farhann1 wants to merge 7 commits into
mainfrom
farhannawaz/rle-control-plane

Conversation

@farhann1

@farhann1 farhann1 commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Changes:

  • Switch environment and sandbox APIs to Foundry project-relative endpoints.
  • Authenticate using Azure credentials and the Foundry token scope.
  • Support versioned publishing with --version-bump.
  • Rename deploy to publish.
  • Add environment list with table and JSON output.
  • Poll asynchronous disk-image conversion before sandbox leasing.
  • Improve sandbox lease, status, and release handling.
  • Keep sandbox data-plane URLs hidden from terminal output.
  • Update documentation, tests, and extension version to 0.2.1-preview.

Copilot AI review requested due to automatic review settings July 28, 2026 14:25
@github-actions

Copy link
Copy Markdown

🔗 Linked Issue Required

Thanks for the contribution! Please link a GitHub issue to this PR by adding Fixes #123 to the description or using the sidebar.
No issue yet? Feel free to create one!

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
21 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Switches the RLE extension to authenticated, project-relative Foundry APIs.

Changes:

  • Adds Foundry authentication, API versioning, and new environment/sandbox routes.
  • Adds disk-image conversion polling and updates invocation behavior.
  • Bumps the preview version and refreshes documentation.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
version.txt Bumps version to 0.2.0-preview.
README.md Documents Foundry setup and workflows.
internal/project/docker.go Clarifies ACR login guidance.
internal/cmd/invoke.go Updates sandbox lifecycle and polling.
internal/cmd/invoke_test.go Tests new invocation routes.
internal/cmd/deploy.go Deploys through the Foundry endpoint.
internal/cmd/client.go Adds authenticated Foundry API client.
internal/cmd/client_test.go Tests authentication and endpoints.
go.mod Promotes Azure identity dependencies.
extension.yaml Updates extension version.
CHANGELOG.md Adds 0.2.0-preview notes.
Comments suppressed due to low confidence (1)

cli/azd/extensions/azure.ai.rle/internal/cmd/client_test.go:80

  • azd-code-reviewer: Use t.Context() for test I/O so the request is canceled if the test ends, as required by the repository's Go 1.26 test pattern (cli/azd/AGENTS.md:356).
	err := client.do(context.Background(), http.MethodGet, environmentCollectionPath, nil, nil)

Comment thread cli/azd/extensions/azure.ai.rle/internal/cmd/client.go
Comment thread cli/azd/extensions/azure.ai.rle/CHANGELOG.md
Comment thread cli/azd/extensions/azure.ai.rle/README.md Outdated
Comment thread cli/azd/extensions/azure.ai.rle/internal/cmd/client.go Outdated
Comment thread cli/azd/extensions/azure.ai.rle/internal/cmd/invoke_test.go Outdated
Comment thread cli/azd/extensions/azure.ai.rle/internal/cmd/client_test.go Outdated
Validate persisted Foundry endpoints before creating credentials and cover the full project-relative route shape in invoke tests. Correct the release heading, prerequisites, and test contexts.

Authored-by: GitHub Copilot CLI v1.0.68
Model: GPT-5.4 (gpt-5.4)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings July 29, 2026 07:51
@farhann1 farhann1 changed the title fix: switch to foundry auth and backend urls fix: switch to foundry backend urls Jul 29, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 11 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (5)

cli/azd/extensions/azure.ai.rle/internal/cmd/client.go:228

  • azd-code-reviewer: GetToken failures are local authentication failures, but both callers wrap this error with serviceError, labeling it rle-control-plane and suggesting that the endpoint is unreachable. A signed-out user therefore gets the wrong remediation. Return a distinct local authentication error with az login/azd auth login guidance before the generic service-error path; azdext.ServiceError is reserved for HTTP/gRPC service failures (cli/azd/pkg/azdext/extension_error.go:15-16).
	token, err := c.credential.GetToken(ctx, policy.TokenRequestOptions{
		Scopes: []string{foundryTokenScope},
	})
	if err != nil {
		return fmt.Errorf("authenticate to Foundry: %w", err)

cli/azd/extensions/azure.ai.rle/internal/cmd/invoke.go:98

  • azd-code-reviewer: Removing the playground URL makes the browser-launch warning below non-actionable: on headless or SSH systems, users can no longer open the UI manually. Keep the raw sandbox URL hidden, but provide a safe fallback such as a localhost proxy URL or an actionable command when OpenBrowser fails.
		"Sandbox %s ready\n",
		sandbox.Id,

cli/azd/extensions/azure.ai.rle/internal/cmd/deploy.go:116

  • azd-code-reviewer: Both branches call the same API, so this condition implies create/update behavior that does not exist and can drift later. Collapse it to one call.
	if state.EnvironmentId == "" {
		environment, err = client.createV1Environment(a.cmd.Context(), request)
	} else {
		environment, err = client.createV1Environment(a.cmd.Context(), request)
	}

cli/azd/extensions/azure.ai.rle/README.md:159

  • The code now intentionally falls back to the project endpoint saved in .azd-rle.json when FOUNDRY_PROJECT_ENDPOINT is unset, but this says deploy always reads the environment variable. Document the fallback and precedence so users know which Foundry project will receive the deployment.
Deploy reads the Foundry project endpoint from `FOUNDRY_PROJECT_ENDPOINT` and the ACR registry from `AZURE_CONTAINER_REGISTRY_ENDPOINT`. It derives the project route segment from `/api/projects/<project>`, builds the Docker image as `<registry>.azurecr.io/<project>-<environment>:latest`, pushes it to ACR, registers that image by calling `<FOUNDRY_PROJECT_ENDPOINT>/fine_tuning/environments`, and saves the project/environment details in `.azd-rle.json`.

cli/azd/extensions/azure.ai.rle/internal/cmd/client.go:88

  • This remediation assumes the endpoint came from FOUNDRY_PROJECT_ENDPOINT, but deploy can now use the value saved in .azd-rle.json. Refer to the configured endpoint generically and explain that the environment variable overrides it; otherwise the message points users at an unset variable.

This issue also appears on line 224 of the same file.

		Suggestion: fmt.Sprintf(
			"Ensure the Foundry project endpoint in %s is reachable and enabled for RLE.",
			foundryProjectEndpointEnvVar,
		),

Copilot AI review requested due to automatic review settings July 29, 2026 11:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 12 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (5)

cli/azd/extensions/azure.ai.rle/internal/cmd/client.go:95

  • normalizeFoundryProjectEndpoint currently accepts paths with extra segments after /api/projects/<project>. That was mostly harmless when only the project segment was extracted, but this client now uses the whole normalized value as its base URL, so an endpoint such as .../api/projects/p/extra sends every request under /extra/fine_tuning/... and fails with a service error instead of being rejected. Require the project endpoint path to end after the project segment before using it as the base URL.
func newRleClient(endpoint string) (*rleClient, error) {
	normalizedEndpoint, err := normalizeFoundryProjectEndpoint(endpoint)
	if err != nil {
		return nil, err

cli/azd/extensions/azure.ai.rle/internal/cmd/run.go:194

  • Removing the runtime override disables /web for valid OpenEnv images whose Dockerfile defaults ENABLE_WEB_INTERFACE=false (for example, upstream grid_world_env and dipg_safety_env). run still always opens and documents /web, so those environments now launch a missing playground. Keep passing ENABLE_WEB_INTERFACE=true to preserve the command's behavior.
		"-p", portMapping,
		image,

cli/azd/extensions/azure.ai.rle/internal/cmd/client.go:228

  • A token acquisition failure is returned as an ordinary client error, so both deploy and invoke wrap it with serviceError. This reports rle-control-plane as the failing service and suggests checking the endpoint even though no request was sent; users who need to sign in or select a credential get misleading recovery guidance. Preserve a distinct auth/local error here and avoid wrapping it as a service response.
	token, err := c.credential.GetToken(ctx, policy.TokenRequestOptions{
		Scopes: []string{foundryTokenScope},
	})
	if err != nil {
		return fmt.Errorf("authenticate to Foundry: %w", err)

cli/azd/extensions/azure.ai.rle/internal/cmd/invoke.go:98

  • The sandbox URL is hidden on the success path, but it is still exposed on failures. project.WaitForHealth includes baseUrl in its timeout error, and RunShellWithContext includes the full request URL on transport errors, so invoke can print the data-plane URL whenever health or a shell request fails. Redact or replace those errors on the remote path to satisfy the stated URL-hiding behavior.
	sandboxUrl := strings.TrimRight(sandbox.BaseUrl, "/")
	if _, err := fmt.Fprintf(
		a.cmd.OutOrStdout(),
		"Sandbox %s ready\n",
		sandbox.Id,

cli/azd/extensions/azure.ai.rle/internal/cmd/invoke.go:178

  • The new 15-minute polling loop aborts on the first transient GET failure. Because this client has no Azure SDK retry policy, a temporary network error, 429, or 5xx during image conversion ends invoke even though conversion may still be progressing. Retry transient failures within the existing deadline (while still returning permanent 4xx responses) so the asynchronous wait is resilient.
	deadline := time.Now().Add(remoteImageConversionTimeout)
	for {
		environment, err := client.getEnvironmentVersion(ctx, state.Name, state.EnvironmentVersion)
		if err != nil {
			return err

Copilot AI review requested due to automatic review settings July 29, 2026 11:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 13 out of 13 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (3)

cli/azd/extensions/azure.ai.rle/internal/cmd/client.go:228

  • Authentication failures here are later passed through serviceError by both deploy and invoke, so they become an azdext.ServiceError attributed to rle-control-plane with advice to check the endpoint. GetToken fails locally before any service call, making the telemetry attribution and remediation incorrect. Preserve credential failures as a local authentication error (with az login/azd auth login guidance), and reserve ServiceError for an actual HTTP response. (azd-code-reviewer)
	token, err := c.credential.GetToken(ctx, policy.TokenRequestOptions{
		Scopes: []string{foundryTokenScope},
	})
	if err != nil {
		return fmt.Errorf("authenticate to Foundry: %w", err)

cli/azd/extensions/azure.ai.rle/internal/cmd/invoke.go:94

  • The sandbox URL is still exposed on failure paths despite the PR's promise to keep it out of terminal output. project.WaitForHealth includes baseUrl in its returned error (runtime.go:145), and RunShellWithContext prints request errors containing the full URL (runtime.go:175-177). A health timeout or transient shell request failure will therefore reveal the data-plane proxy URL. Add a remote-sandbox redaction path so these helpers never render this URL. (azd-code-reviewer)
	sandboxUrl := strings.TrimRight(sandbox.BaseUrl, "/")

cli/azd/extensions/azure.ai.rle/internal/cmd/run.go:194

  • Removing this override breaks run's documented promise to open /web for custom OpenEnv images. OpenEnv's web UI is off by default unless ENABLE_WEB_INTERFACE=true, and current upstream images such as grid_world_env and dipg_safety_env explicitly bake in false; those containers will remain healthy, but the browser opens a missing UI. Keep the runtime override so azd ai rle run consistently enables the playground. (azd-code-reviewer)
		"run", "-d",
		"--name", container,
		"--label", localContainerImageLabel + "=" + image,
		"-p", portMapping,
		image,

Comment thread cli/azd/extensions/registry.dev.json Outdated
Copilot AI review requested due to automatic review settings July 30, 2026 07:48

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 19 out of 19 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (5)

cli/azd/extensions/azure.ai.rle/internal/cmd/publish.go:126

  • [azd-code-reviewer] Both branches perform the same POST, so the local EnvironmentId check no longer affects the request. Collapse this to one call; keeping a duplicated create/update branch suggests distinct behavior that does not exist and makes future changes easy to apply to only one copy.
    cli/azd/extensions/azure.ai.rle/internal/cmd/environment.go:132
  • [azd-code-reviewer] go fix ./... will rewrite this errors.As usage to errors.AsType, and the repository preflight enforces that modernization (cli/azd/AGENTS.md:332-358). Use the typed helper here so this new file passes the enforced check.
		var localErr *azdext.LocalError
		if !errors.As(stateErr, &localErr) || localErr.Code != "rle_project_not_initialized" {
			return "", stateErr

cli/azd/extensions/azure.ai.rle/internal/cmd/invoke.go:94

  • [azd-code-reviewer] The proxy URL is still exposed on error paths. project.WaitForHealth includes baseUrl in its returned message (internal/project/runtime.go:145), and shell request failures print the full operation URL (runtime.go:177 via lines 106-108). A failed health check or sandbox request therefore writes sandbox.BaseUrl to the terminal despite the new redaction requirement; redact it in those errors or translate remote errors before displaying them.
	sandboxUrl := strings.TrimRight(sandbox.BaseUrl, "/")

cli/azd/extensions/azure.ai.rle/internal/cmd/invoke.go:178

  • [azd-code-reviewer] The new 15-minute polling loop aborts on the first transient GET failure. Because rleClient uses a plain http.Client with no retry policy, a single 429 or 5xx while conversion is still progressing makes invoke fail immediately instead of continuing until the conversion deadline. Treat retryable HTTP/transport errors as another pending poll (while still honoring ctx and the deadline).
		environment, err := client.getEnvironmentVersion(ctx, state.Name, state.EnvironmentVersion)
		if err != nil {
			return err

cli/azd/extensions/azure.ai.rle/internal/cmd/client.go:100

  • [azd-code-reviewer] This normalization does not guarantee a project-relative base URL. projectNameFromFoundryEndpoint accepts any path with at least three segments, so an endpoint such as /api/projects/p/extra passes validation; the new client then appends /fine_tuning/environments after /extra, guaranteeing requests go to the wrong route. Require exactly /api/projects/<project> (or canonicalize to those three segments) before storing baseUrl.
func newRleClient(endpoint string) (*rleClient, error) {
	normalizedEndpoint, err := normalizeFoundryProjectEndpoint(endpoint)
	if err != nil {

Copilot AI review requested due to automatic review settings July 30, 2026 07:59
@farhann1
farhann1 requested a review from trangevi July 30, 2026 08:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated no new comments.

Comments suppressed due to low confidence (4)

cli/azd/extensions/azure.ai.rle/internal/cmd/publish.go:125

  • Both branches issue the same POST, so this conditional no longer selects create versus update behavior and can drift as the client evolves. Call createV1Environment once and keep created only for the user-facing label. (azd-code-reviewer)
    cli/azd/extensions/azure.ai.rle/internal/cmd/invoke.go:178
  • A single transient polling failure (for example HTTP 429/503 or a brief network error) aborts the entire 15-minute conversion wait immediately. Since this loop is specifically waiting for an asynchronous Azure operation, retry transient failures until the existing deadline and honor Retry-After; only permanent responses should return here. (azd-code-reviewer)
		environment, err := client.getEnvironmentVersion(ctx, state.Name, state.EnvironmentVersion)
		if err != nil {
			return err

cli/azd/extensions/azure.ai.rle/internal/cmd/environment.go:131

  • Use errors.AsType[*azdext.LocalError](stateErr) here. This repository requires the Go 1.26 errors.AsType pattern instead of errors.As, and leaving this form causes the new code to fail the enforced modernization convention. (azd-code-reviewer)
		var localErr *azdext.LocalError
		if !errors.As(stateErr, &localErr) || localErr.Code != "rle_project_not_initialized" {
			return "", stateErr

cli/azd/extensions/azure.ai.rle/internal/cmd/invoke.go:94

  • The data-plane URL is still exposed on failure. project.WaitForHealth formats baseUrl into its returned LocalError (runtime.go:145), and RunShellWithContext later prints transport errors containing the full request URL (runtime.go:177). Thus an unhealthy or disconnected sandbox puts this URL back in terminal output despite this PR's redaction requirement. Please add a remote/redacted error path for both helpers. (azd-code-reviewer)

This issue also appears on line 176 of the same file.

	sandboxUrl := strings.TrimRight(sandbox.BaseUrl, "/")

@jongio jongio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things worth a second look here, plus a few nits.

  • publish.go:122-126 both branches of the create/update conditional call the same thing, so either the update path is missing or the branch should go.
  • publish never writes the server's environment name back into state, and invoke keys its disk-image poll off that name.

Nits: the 0.2.0-preview changelog heading is invisible to the changelog tooling, resolvePublishState and resolvePublishImage take a flags argument neither reads, and the paging loop in environment list has no upper bound.

Separately, PR Governance is red because there's no linked issue on this PR.

Comment on lines 122 to 126
if state.EnvironmentId == "" {
environment, err = client.createV1Environment(a.cmd.Context(), projectName, request)
environment, err = client.createV1Environment(a.cmd.Context(), request)
} else {
environment, err = client.updateV1Environment(a.cmd.Context(), projectName, environmentId, request)
if isNotFoundError(err) {
// The recorded environment no longer exists in the target project
// (e.g. the project changed or the control plane was reset). Recreate it.
if _, msgErr := fmt.Fprintf(
a.cmd.OutOrStdout(),
"Environment '%s' not found in project '%s'; creating a new one.\n",
environmentId,
projectName,
); msgErr != nil {
return msgErr
}
created = true
environment, err = client.createV1Environment(a.cmd.Context(), projectName, request)
}
environment, err = client.createV1Environment(a.cmd.Context(), request)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both branches call createV1Environment with the same request, so this conditional doesn't do anything. The action and label variables right above switch between "Creating" and "Updating", which reads like an update call was meant to land in the else branch.

If the control plane really does handle create-or-update through the same POST, the branch can just go:

Suggested change
if state.EnvironmentId == "" {
environment, err = client.createV1Environment(a.cmd.Context(), projectName, request)
environment, err = client.createV1Environment(a.cmd.Context(), request)
} else {
environment, err = client.updateV1Environment(a.cmd.Context(), projectName, environmentId, request)
if isNotFoundError(err) {
// The recorded environment no longer exists in the target project
// (e.g. the project changed or the control plane was reset). Recreate it.
if _, msgErr := fmt.Fprintf(
a.cmd.OutOrStdout(),
"Environment '%s' not found in project '%s'; creating a new one.\n",
environmentId,
projectName,
); msgErr != nil {
return msgErr
}
created = true
environment, err = client.createV1Environment(a.cmd.Context(), projectName, request)
}
environment, err = client.createV1Environment(a.cmd.Context(), request)
}
environment, err = client.createV1Environment(a.cmd.Context(), request)

If not, the else branch needs the real update call.

deadline := time.Now().Add(remoteImageConversionTimeout)
for {
project, err := projectRouteSegment(state)
environment, err := client.getEnvironmentVersion(ctx, state.Name, state.EnvironmentVersion)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This polls /fine_tuning/environments/{state.Name}/versions/{version}, but publish only writes environment.Id and environment.Version back to state. It leaves state.Name at whatever the local project derived. The publish JSON output prints environment.Name from the response while the console line prints state.Name, so the service clearly hands back a name of its own.

If the service ever normalizes or slugs that name on create, this GET 404s and invoke dies after the 15 minute conversion deadline with a confusing error. Setting state.Name from the publish response alongside the id and version would close that gap. Is the returned name guaranteed to match what was sent?

return nil, err
}
environments = append(environments, page.Value...)
if len(page.Value) < environmentListPageSize {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing caps this loop. The only exit is a short page, so if the service ignores skip or top and keeps returning a full page, environment list spins until the user hits Ctrl+C while all grows the whole time. A max-page or max-item guard is a couple of lines and turns a hang into an error.

}
}

func resolvePublishState(flags *rlePublishFlags) (rleState, bool, error) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flags isn't read anywhere in this function, and resolvePublishImage takes it without reading it either. Both can drop the parameter.

- Rename `azd ai rle deploy` to `azd ai rle publish` to avoid confusion with the core `azd deploy` command.
- Add `--version-bump` to `azd ai rle publish` so users can choose major, minor, or patch environment versioning.

## 0.2.0-preview

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

eng/common/scripts/ChangeLog-Operations.ps1 builds its release-title regex with a required parenthesized status group, so a heading without one doesn't match and this section is invisible to the changelog tooling. 0.2.1-preview (Unreleased) above is fine. This one needs either a release date or (Unreleased) depending on whether you consider 0.2.0-preview shipped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ext-rle azure.ai.rle extension

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants