Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/fx-core/resource/package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -1002,6 +1002,8 @@
"error.deploy.ZipFileTargetInUse": "Unable to clear the distribution zip file in %s as it may be currently in use. Close any apps using the file and try again.",
"error.deploy.GetPublishingCredentialsError.Notification": "Unable to obtain publishing credentials of app '%s' in resource group '%s'. Refer to the [Output panel](command:fx-extension.showOutputChannel) for more details.",
"error.deploy.GetPublishingCredentialsError": "Unable to obtain publishing credentials of app '%s' in resource group '%s' for reason:\n %s.\n Suggestions:\n 1. Make sure the app name and resource group name are spelled correctly and are valid. \n 2. Make sure your Azure account has necessary permissions to access the API. You may need to elevate your role or request additional permissions from an administrator. \n 3. If the error message includes a specific reason, such as an authentication failure or a network issue, investigate that issue specifically to resolve the error and try again. \n 4. You can test the API in this page: '%s'",
"error.deploy.GetZipDeployEndpointError.Notification": "Unable to get zip deploy endpoint of app '%s' in resource group '%s'. Refer to the [Output panel](command:fx-extension.showOutputChannel) for more details.",
"error.deploy.GetZipDeployEndpointError": "Unable to get zip deploy endpoint of app '%s' in resource group '%s' due to error: %s.",
"error.deploy.DeployZipPackageError.Notification": "Unable to deploy zip package to endpoint: '%s'. Refer to the [Output panel](command:fx-extension.showOutputChannel) for more details and try again.",
"error.deploy.DeployZipPackageError": "Unable to deploy zip package to endpoint '%s' in Azure due to error: %s. \nSuggestions:\n 1. Make sure your Azure account has necessary permissions to access the API. \n 2. Make sure the endpoint is properly configured in Azure and the required resources have been provisioned. \n 3. Make sure the zip package is valid and free of errors. \n 4. If the error message specifies the reason, such as an authentication failure or a network issue, fix the error and try again. \n 5. If the error still persists, deploy the package manually following the guidelines in this link: '%s'",
"error.deploy.CheckDeploymentStatusError": "Unable to check deployment status for location: '%s' due to error: %s. If the issue persists, review the deployment logs (Deployment -> Deployment center -> Logs) in Azure portal to identify any issues that may have occurred.",
Expand Down Expand Up @@ -1150,4 +1152,4 @@
"core.MCPForDA.mcpAuthMetadataUrlNotFound": "Unable to find the authentication metadata from property \"resource_metadata\" from response of the MCP server.",
"core.MCPForDA.mcpServerMetadataUrlNotFound": "Unable to find the server metadata from property \"authorization_servers\" from repsonse of the Protected Resource Metadata of the MCP server.",
"core.MCPForDA.authUrlNotFound": "Unable to find the authentication URL(s) from response of Authorization Server Metadata of the MCP server."
}
}
4 changes: 3 additions & 1 deletion packages/fx-core/resource/package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,8 @@
"error.deploy.ZipFileTargetInUse": "无法清除 %s 中的分发 zip 文件,因为它当前可能正在使用中。请关闭使用该文件的任何应用,然后重试。",
"error.deploy.GetPublishingCredentialsError.Notification": "无法获取资源组 \"%s\" 中应用 \"%s\" 的发布凭据。有关更多详细信息,请参阅 [输出面板](command:fx-extension.showOutputChannel)。",
"error.deploy.GetPublishingCredentialsError": "无法获取应用“%s”的发布凭据(在资源组“%s”中),原因是:\n %s。\n 建议:\n 1. 确保应用名称和资源组名称拼写正确且有效。\n2. 确保 Azure 帐户具有访问 API 所需的权限。可能需要提升角色或向管理员请求其他权限。\n3. 如果错误消息显示了特定原因(例如身份验证失败或网络问题),请专门调查该问题以解决该错误,然后重试。\n4. 可在此页中测试 API:“%s”",
"error.deploy.GetZipDeployEndpointError.Notification": "无法获取应用 \"%s\" (在资源组 \"%s\" 中)的 zip 部署终结点。有关更多详细信息,请参阅 [输出面板](command:fx-extension.showOutputChannel)。",
"error.deploy.GetZipDeployEndpointError": "无法获取应用“%s”的 zip 部署终结点(在资源组“%s”中),错误为: %s。",
"error.deploy.DeployZipPackageError.Notification": "无法将 zip 包部署到终结点: \"%s\"。有关更多详细信息,请参阅 [输出面板](command:fx-extension.showOutputChannel),然后重试。",
"error.deploy.DeployZipPackageError": "无法将 zip 包部署到 Azure 中的终结点“%s”,因为出现错误: %s。\n建议:\n 1. 确保 Azure 帐户具有访问 API 所需的权限。\n2. 确保在 Azure 中正确配置了终结点,并且已预配所需的资源。\n3. 确保 zip 包有效且没有错误。\n4. 如果错误消息指定了身份验证失败或网络问题等原因,请修复错误并重试。\n5. 如果错误仍然存在,请按照以下链接中的准则手动部署包:“%s”",
"error.deploy.CheckDeploymentStatusError": "无法检查位置“%s”的部署状态,因为出现错误 %s。如果问题仍然存在,请查看 Azure 门户中的部署日志(部署 -> 部署中心 -> 日志),以确定可能出现的任何问题。",
Expand Down Expand Up @@ -924,4 +926,4 @@
"error.dep.FindProcessError": "找不到按 pid 或端口) 的进程(es。%s",
"error.kiota.FailedToCreateAdaptiveCard": "Unable to generate adaptive card in plugin manifest. Manually update the manifest file, if required.",
"error.kiota.FailedToGenerateAuthActions": "Unable to parse Open API spec and generate Auth actions in teamsapp.yml. Manually update the yml files, if required."
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import { HttpStatusCode } from "../../../../constant/commonConstant";
import { getLocalizedString } from "../../../../../common/localizeUtils";
import path from "path";
import { zipFolderAsync } from "../../../../utils/fileOperation";
import { DeployZipPackageError } from "../../../../../error/deploy";
import { DeployZipPackageError, GetZipDeployEndpointError } from "../../../../../error/deploy";
import { ErrorContextMW } from "../../../../../common/globalVars";
import { hooks } from "@feathersjs/hooks";
import { ReadStream } from "fs-extra";
Expand Down Expand Up @@ -247,25 +247,147 @@ export class AzureZipDeployImpl extends AzureDeployImpl {
azureResource: AzureResourceInfo,
config: AzureUploadConfig
): Promise<string> {
const response = await fetch(
`${getResourceServiceEndpoint(ResourceServiceType.Azure)}/subscriptions/${
azureResource.subscriptionId
}/resourceGroups/${azureResource.resourceGroupName}/providers/Microsoft.Web/sites/${
azureResource.instanceId
}?api-version=2024-04-01`,
{
const resourceUrl = `${getResourceServiceEndpoint(ResourceServiceType.Azure)}/subscriptions/${
azureResource.subscriptionId
}/resourceGroups/${azureResource.resourceGroupName}/providers/Microsoft.Web/sites/${
azureResource.instanceId
}?api-version=2024-04-01`;
let response: Response;
try {
response = await fetch(resourceUrl, {
headers: config.headers,
}
);
const responseData = await response.json();
const hostNames: string[] = responseData.properties.enabledHostNames;
});
} catch (e) {
throw new GetZipDeployEndpointError(
azureResource.instanceId,
azureResource.resourceGroupName,
AzureZipDeployImpl.normalizeError(e)
);
}

let responseText = "";
try {
responseText = await response.text();
} catch {
throw new GetZipDeployEndpointError(
azureResource.instanceId,
azureResource.resourceGroupName,
new Error(
`Failed to read response body. Status code: ${response.status}, status text: ${
response.statusText || "NA"
}.`
)
);
}

if (!response.ok) {
const responsePreview = AzureZipDeployImpl.previewResponseText(responseText);
throw new GetZipDeployEndpointError(
azureResource.instanceId,
azureResource.resourceGroupName,
new Error(
`Failed to get Azure App Service details. Status code: ${response.status}, status text: ${
response.statusText || "NA"
}, response body: ${responsePreview}`
)
);
}

let responseData: unknown;
try {
responseData = responseText ? JSON.parse(responseText) : {};
} catch {
throw new GetZipDeployEndpointError(
azureResource.instanceId,
azureResource.resourceGroupName,
new Error(
`Failed to parse Azure App Service details response as JSON. Response body: ${AzureZipDeployImpl.previewResponseText(
responseText
)}`
)
);
}

const responseError = AzureZipDeployImpl.getResponseErrorMessage(responseData);
if (responseError) {
throw new GetZipDeployEndpointError(
azureResource.instanceId,
azureResource.resourceGroupName,
new Error(`Azure App Service API returned error payload: ${responseError}`)
);
}

let hostNames: string[];
try {
hostNames = AzureZipDeployImpl.getEnabledHostNames(responseData);
} catch (e) {
throw new GetZipDeployEndpointError(
azureResource.instanceId,
azureResource.resourceGroupName,
AzureZipDeployImpl.normalizeError(e)
);
}
const scmHostName = hostNames.find((host) => host.includes("scm"));
if (!scmHostName) {
throw new Error(`Cannot find SCM host name. Available host names: ${hostNames.join(", ")}`);
throw new GetZipDeployEndpointError(
azureResource.instanceId,
azureResource.resourceGroupName,
new Error(`Cannot find SCM host name. Available host names: ${hostNames.join(", ")}`)
);
}
return `https://${scmHostName}/api/zipdeploy?isAsync=true`;
}

private static getEnabledHostNames(responseData: unknown): string[] {
if (
!AzureZipDeployImpl.isRecord(responseData) ||
!AzureZipDeployImpl.isRecord(responseData.properties)
) {
throw new Error("Invalid Azure App Service details response. Missing 'properties' object.");
}
const hostNames = responseData.properties.enabledHostNames;
if (!Array.isArray(hostNames) || hostNames.some((host) => typeof host !== "string")) {
throw new Error(
"Invalid Azure App Service details response. 'properties.enabledHostNames' must be a string array."
);
}
return hostNames;
}

private static getResponseErrorMessage(responseData: unknown): string | undefined {
if (
!AzureZipDeployImpl.isRecord(responseData) ||
!AzureZipDeployImpl.isRecord(responseData.error)
) {
return undefined;
}
const code = typeof responseData.error.code === "string" ? responseData.error.code : "NA";
const message =
typeof responseData.error.message === "string" ? responseData.error.message : "NA";
return `code: ${code}, message: ${message}`;
}

private static isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}

private static normalizeError(error: unknown): Error {
if (error instanceof Error) {
return error;
}
return new Error(String(error));
}

private static previewResponseText(responseText: string): string {
const maxPreviewLength = 200;
if (!responseText) {
return "NA";
}
return responseText.length > maxPreviewLength
? `${responseText.slice(0, maxPreviewLength)}...`
: responseText;
}

updateProgressbar(): void {
this.progressBar?.next(ProgressMessages.deployToAzure(this.workingDirectory, this.serviceName));
}
Expand Down
22 changes: 22 additions & 0 deletions packages/fx-core/src/error/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ export class GetPublishingCredentialsError extends UserError {
}
}

export class GetZipDeployEndpointError extends UserError {
constructor(appName: string, resourceGroup: string, error: Error, helpLink?: string) {
super({
source: "azureDeploy",
message: getDefaultString(
"error.deploy.GetZipDeployEndpointError",
appName,
resourceGroup,
stringifyError(error)
),
displayMessage: getLocalizedString(
"error.deploy.GetZipDeployEndpointError.Notification",
appName,
resourceGroup
),
helpLink: helpLink,
error: error,
categories: [ErrorCategory.External],
});
}
}

export class DeployZipPackageError extends UserError {
constructor(endpoint: string, error: Error, helpLink?: string) {
super({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {
CheckDeploymentStatusError,
CheckDeploymentStatusTimeoutError,
DeployZipPackageError,
GetZipDeployEndpointError,
GetPublishingCredentialsError,
} from "../../../../../src/error/deploy";
import * as chai from "chai";
Expand Down Expand Up @@ -157,7 +158,9 @@ describe("AzureDeployImpl zip deploy acceleration", () => {
{ status: 200 }
)
);
chai.expect(AzureZipDeployImpl.getZipDeployEndpoint(ar, config)).to.be.rejectedWith(Error);
chai
.expect(AzureZipDeployImpl.getZipDeployEndpoint(ar, config))
.to.be.rejectedWith(GetZipDeployEndpointError);
chai.expect(fetchStub.calledOnce).to.be.true;
chai
.expect(fetchStub.firstCall.args[0])
Expand All @@ -166,6 +169,100 @@ describe("AzureDeployImpl zip deploy acceleration", () => {
);
});

it("Get zip deploy endpoint with non-success status", async () => {
const ar = {
subscriptionId: "aaa",
resourceGroupName: "bbb",
instanceId: "ccc",
} as AzureResourceInfo;
const config = {
headers: {
"Content-Type": "AAA",
"Cache-Control": "bbb",
Authorization: "ccc",
},
maxContentLength: 1,
maxBodyLength: 2,
timeout: 3,
} as AzureUploadConfig;
const fetchStub = sandbox.stub(global, "fetch");
fetchStub.resolves(new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }));
await chai
.expect(AzureZipDeployImpl.getZipDeployEndpoint(ar, config))
.to.be.rejectedWith(
GetZipDeployEndpointError,
"Failed to get Azure App Service details. Status code: 401"
);
chai.expect(fetchStub.calledOnce).to.be.true;
});

it("Get zip deploy endpoint with Azure error payload in success response", async () => {
const ar = {
subscriptionId: "aaa",
resourceGroupName: "bbb",
instanceId: "ccc",
} as AzureResourceInfo;
const config = {
headers: {
"Content-Type": "AAA",
"Cache-Control": "bbb",
Authorization: "ccc",
},
maxContentLength: 1,
maxBodyLength: 2,
timeout: 3,
} as AzureUploadConfig;
const fetchStub = sandbox.stub(global, "fetch");
fetchStub.resolves(
new Response(
JSON.stringify({
error: {
code: "AuthorizationFailed",
message: "The client does not have authorization to perform action.",
},
}),
{ status: 200 }
)
);
await chai
.expect(AzureZipDeployImpl.getZipDeployEndpoint(ar, config))
.to.be.rejectedWith(GetZipDeployEndpointError, "AuthorizationFailed");
chai.expect(fetchStub.calledOnce).to.be.true;
});

it("Get zip deploy endpoint with malformed host names response", async () => {
const ar = {
subscriptionId: "aaa",
resourceGroupName: "bbb",
instanceId: "ccc",
} as AzureResourceInfo;
const config = {
headers: {
"Content-Type": "AAA",
"Cache-Control": "bbb",
Authorization: "ccc",
},
maxContentLength: 1,
maxBodyLength: 2,
timeout: 3,
} as AzureUploadConfig;
const fetchStub = sandbox.stub(global, "fetch");
fetchStub.resolves(
new Response(
JSON.stringify({
properties: {
enabledHostNames: "not-array",
},
}),
{ status: 200 }
)
);
await chai
.expect(AzureZipDeployImpl.getZipDeployEndpoint(ar, config))
.to.be.rejectedWith(GetZipDeployEndpointError, "enabledHostNames");
chai.expect(fetchStub.calledOnce).to.be.true;
});

it("checkDeployStatus empty response", async () => {
sandbox.stub(AzureDeployImpl.AXIOS_INSTANCE, "get").resolves("");
const config = {
Expand Down