From ea8f0bcd22f216de6fc9fa4dd78a3c827f66ca74 Mon Sep 17 00:00:00 2001 From: "Alex Yang [MSFT]" <59073590+alexyaang@users.noreply.github.com> Date: Wed, 23 Aug 2023 15:32:39 -0400 Subject: [PATCH] Added Back Azure Deploy to ACA & Azure Deploy to AAS (#4038) * added back basic implementation of deploy ACA * saving progress * added step for selecting subscriptions * small tweaks * progress on deploy image to azure app service * improved azure subscription selection experience * actually made deploy image to AAS work * remove accidetal change to settings.json * fix small mistake * fixed more mistakes * fix more mistakes * fixed even more mistakes... --- package-lock.json | 208 +++++++++++-- package.json | 10 +- src/commands/registerCommands.ts | 6 +- .../azure/DockerAssignAcrPullRoleStep.ts | 180 ++++++------ .../registries/azure/DockerSiteCreateStep.ts | 275 +++++++++--------- .../azure/DockerWebhookCreateStep.ts | 177 +++++------ .../azure/WebSitesPortPromptStep.ts | 56 ++-- .../registries/azure/deployImageToAca.ts | 192 ++++++------ .../registries/azure/deployImageToAzure.ts | 134 +++++---- src/extensionVariables.ts | 2 + src/tree/registerTrees.ts | 4 +- .../Azure/AzureRegistryDataProvider.ts | 19 +- src/tree/registries/registryTreeUtils.ts | 10 + src/utils/azureUtils.ts | 53 ---- src/utils/registryExperience.ts | 24 ++ 15 files changed, 759 insertions(+), 591 deletions(-) create mode 100644 src/utils/registryExperience.ts diff --git a/package-lock.json b/package-lock.json index 324f739002..955306476e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,10 @@ "@azure/arm-containerregistry": "^10.1.0", "@azure/storage-blob": "^12.14.0", "@microsoft/compose-language-service": "^0.2.0", - "@microsoft/vscode-azext-azureappservice": "^1.0.2", + "@microsoft/vscode-azext-azureappservice": "^2.2.0", "@microsoft/vscode-azext-azureauth": "^1.1.2", - "@microsoft/vscode-azext-azureutils": "^1.1.5", - "@microsoft/vscode-azext-utils": "^1.2.2", + "@microsoft/vscode-azext-azureutils": "^2.0.1", + "@microsoft/vscode-azext-utils": "^2.0.1", "@microsoft/vscode-container-client": "^0.1.0", "@microsoft/vscode-docker-registries": "file:../vscode-docker-extensibility/packages/vscode-docker-registries/microsoft-vscode-docker-registries-0.0.1-alpha.tgz", "dayjs": "^1.11.7", @@ -681,9 +681,9 @@ "integrity": "sha512-n1VPsljTSkthsAFYdiWfC+DKzK2WwcRp83Y1YAqdX552BstvsDjft9YXppjUzp11BPsapDoO1LDgrDB0XVsfNQ==" }, "node_modules/@microsoft/vscode-azext-azureappservice": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureappservice/-/vscode-azext-azureappservice-1.0.2.tgz", - "integrity": "sha512-YaKqYIkeX0kH8KgUsUOUrr7RA9ghtFnZXcS8RfSIs6/HmssN3pYdKEjlq4Yq3U5oOkX8Cq5xlujHoCiUaUsh7Q==", + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureappservice/-/vscode-azext-azureappservice-2.2.5.tgz", + "integrity": "sha512-pnbunSuGd0W1XojN7P/sNcdMeVC1FttXYR9UY4tNgwgstz+zeN5ckYtwWTHdu4fTP+uEczpHKz0zGf6Mn4fkfw==", "dependencies": { "@azure/abort-controller": "^1.0.4", "@azure/arm-appinsights": "^5.0.0-beta.4", @@ -694,8 +694,9 @@ "@azure/core-client": "^1.7.2", "@azure/core-rest-pipeline": "^1.10.3", "@azure/storage-blob": "^12.3.0", - "@microsoft/vscode-azext-azureutils": "^1.1.5", - "@microsoft/vscode-azext-utils": "^1.2.2", + "@microsoft/vscode-azext-azureutils": "^2.0.2", + "@microsoft/vscode-azext-github": "^1.0.0", + "@microsoft/vscode-azext-utils": "^2.0.0", "dayjs": "^1.11.2", "fs-extra": "^10.0.0", "globby": "^11.0.2", @@ -707,7 +708,7 @@ }, "peerDependencies": { "@azure/ms-rest-azure-env": "^2.0.0", - "@microsoft/vscode-azext-azureappsettings": "^0.1.0" + "@microsoft/vscode-azext-azureappsettings": "^0.2.0" } }, "node_modules/@microsoft/vscode-azext-azureappservice/node_modules/fs-extra": { @@ -724,12 +725,12 @@ } }, "node_modules/@microsoft/vscode-azext-azureappsettings": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureappsettings/-/vscode-azext-azureappsettings-0.1.0.tgz", - "integrity": "sha512-oxq3tYgb9yt/Vxh8larmd3XTRW0FEaIfhtzEpZyb0YcqFaTEhY299J9oogiu78+dGDztgR3y8g0AhDHBpEmCiQ==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureappsettings/-/vscode-azext-azureappsettings-0.2.0.tgz", + "integrity": "sha512-fHv+m+dOluuYgPCQ7Mt8HoDgguWy8zHWofP3T6uxkuDF8VAJbjl9LFYHwV0frVcK4Qgxcj95QDjw5AMSUMHtqw==", "peer": true, "dependencies": { - "@microsoft/vscode-azext-utils": "^1.2.1" + "@microsoft/vscode-azext-utils": "^2.0.0" } }, "node_modules/@microsoft/vscode-azext-azureauth": { @@ -742,9 +743,9 @@ } }, "node_modules/@microsoft/vscode-azext-azureutils": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureutils/-/vscode-azext-azureutils-1.1.5.tgz", - "integrity": "sha512-iG89BMp57ydHHl3NbW+T9vn/zksDOoYxgebAZmoF6fZzrIJn2VVmzWGllBsfBa1vEx/qDrvdfT6juSY0UtLe0w==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureutils/-/vscode-azext-azureutils-2.0.2.tgz", + "integrity": "sha512-r+7NqedkvfFazztO7kxT1AU/B/UfiGhTDlRtFHx+W5bhp+yJw0eOtX8VPJXXSXoOaq2dzuGBwxAQ0bDD1lNPcQ==", "dependencies": { "@azure/arm-resources": "^5.0.0", "@azure/arm-resources-profile-2020-09-01-hybrid": "^2.0.0", @@ -754,7 +755,7 @@ "@azure/core-client": "^1.6.0", "@azure/core-rest-pipeline": "^1.9.0", "@azure/logger": "^1.0.4", - "@microsoft/vscode-azext-utils": "^1.2.2", + "@microsoft/vscode-azext-utils": "^2.0.0", "semver": "^7.3.7", "uuid": "^9.0.0" }, @@ -770,10 +771,19 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@microsoft/vscode-azext-github": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-github/-/vscode-azext-github-1.0.0.tgz", + "integrity": "sha512-LYZ5fN0Yv2zfBy8uNuIL/JaXKf8aw+QfW64fuIDknt9qs7UZtFzfTvOimTB5nGd1oN4o7cpthcZg3YHOH2YpKg==", + "dependencies": { + "@microsoft/vscode-azext-utils": "^2.0.0", + "@octokit/rest": "^18.5.2" + } + }, "node_modules/@microsoft/vscode-azext-utils": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-1.2.2.tgz", - "integrity": "sha512-mOTcJF8IMsz+Xn8QTUP1AC3K5tPl/3f17L2xGTTtLeV/HJ2sTh/3712NFuN58tnOwdISdazId4tHwaqUta8HEA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-2.0.5.tgz", + "integrity": "sha512-DNyORyHfPuohBy7Z0rD6YRa4iNLW2LpA+lAv75E9NQ3c1ZhO27tQ2EHj4sYowAqrKZXlqvPHvgjYFj3Dqu5eOQ==", "dependencies": { "@microsoft/vscode-azureresources-api": "^2.0.4", "@vscode/extension-telemetry": "^0.6.2", @@ -814,9 +824,10 @@ "node_modules/@microsoft/vscode-docker-registries": { "version": "0.0.1-alpha", "resolved": "file:../vscode-docker-extensibility/packages/vscode-docker-registries/microsoft-vscode-docker-registries-0.0.1-alpha.tgz", - "integrity": "sha512-iUMbTBE3+s6uWg/Upy8pl8x+JXBp74JmFDS62p60JcJyhl+pPNs2rp+cxLIrl4mH9Ml6vlZeZZCeTJJdq/H+Nw==", + "integrity": "sha512-AUa6YyRJgGzc96gJylSiI+EGm0B5s28LS7Hi8yHH4aIMz8vU/yK5O9PWrt/GxIxm2AHP4oFqQ59lWy2mk2xunA==", "license": "See LICENSE in the project root for license information.", "dependencies": { + "dayjs": "^1.11.7", "node-fetch": "^2.6.11" } }, @@ -852,6 +863,142 @@ "node": ">= 8" } }, + "node_modules/@octokit/auth-token": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.5.0.tgz", + "integrity": "sha512-r5FVUJCOLl19AxiuZD2VRZ/ORjp/4IN98Of6YJoJOkY75CIBuYfmiNHGrDwXr+aLGG55igl9QrxX3hbiXlLb+g==", + "dependencies": { + "@octokit/types": "^6.0.3" + } + }, + "node_modules/@octokit/core": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-3.6.0.tgz", + "integrity": "sha512-7RKRKuA4xTjMhY+eG3jthb3hlZCsOwg3rztWh75Xc+ShDWOfDDATWbeZpAHBNRpm4Tv9WgBMOy1zEJYXG6NJ7Q==", + "dependencies": { + "@octokit/auth-token": "^2.4.4", + "@octokit/graphql": "^4.5.8", + "@octokit/request": "^5.6.3", + "@octokit/request-error": "^2.0.5", + "@octokit/types": "^6.0.3", + "before-after-hook": "^2.2.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/endpoint": { + "version": "6.0.12", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-6.0.12.tgz", + "integrity": "sha512-lF3puPwkQWGfkMClXb4k/eUT/nZKQfxinRWJrdZaJO85Dqwo/G0yOC434Jr2ojwafWJMYqFGFa5ms4jJUgujdA==", + "dependencies": { + "@octokit/types": "^6.0.3", + "is-plain-object": "^5.0.0", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/endpoint/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@octokit/graphql": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.8.0.tgz", + "integrity": "sha512-0gv+qLSBLKF0z8TKaSKTsS39scVKF9dbMxJpj3U0vC7wjNWFuIpL/z76Qe2fiuCbDRcJSavkXsVtMS6/dtQQsg==", + "dependencies": { + "@octokit/request": "^5.6.0", + "@octokit/types": "^6.0.3", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "12.11.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-12.11.0.tgz", + "integrity": "sha512-VsXyi8peyRq9PqIz/tpqiL2w3w80OgVMwBHltTml3LmVvXiphgeqmY9mvBw9Wu7e0QWk/fqD37ux8yP5uVekyQ==" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "2.21.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.21.3.tgz", + "integrity": "sha512-aCZTEf0y2h3OLbrgKkrfFdjRL6eSOo8komneVQJnYecAxIej7Bafor2xhuDJOIFau4pk0i/P28/XgtbyPF0ZHw==", + "dependencies": { + "@octokit/types": "^6.40.0" + }, + "peerDependencies": { + "@octokit/core": ">=2" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.4.tgz", + "integrity": "sha512-mLUsMkgP7K/cnFEw07kWqXGF5LKrOkD+lhCrKvPHXWDywAwuDUeDwWBpc69XK3pNX0uKiVt8g5z96PJ6z9xCFA==", + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "5.16.2", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-5.16.2.tgz", + "integrity": "sha512-8QFz29Fg5jDuTPXVtey05BLm7OB+M8fnvE64RNegzX7U+5NUXcOcnpTIK0YfSHBg8gYd0oxIq3IZTe9SfPZiRw==", + "dependencies": { + "@octokit/types": "^6.39.0", + "deprecation": "^2.3.1" + }, + "peerDependencies": { + "@octokit/core": ">=3" + } + }, + "node_modules/@octokit/request": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-5.6.3.tgz", + "integrity": "sha512-bFJl0I1KVc9jYTe9tdGGpAMPy32dLBXXo1dS/YwSCTL/2nd9XeHsY616RE3HPXDVk+a+dBuzyz5YdlXwcDTr2A==", + "dependencies": { + "@octokit/endpoint": "^6.0.1", + "@octokit/request-error": "^2.1.0", + "@octokit/types": "^6.16.1", + "is-plain-object": "^5.0.0", + "node-fetch": "^2.6.7", + "universal-user-agent": "^6.0.0" + } + }, + "node_modules/@octokit/request-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.1.0.tgz", + "integrity": "sha512-1VIvgXxs9WHSjicsRwq8PlR2LR2x6DwsJAaFgzdi0JfJoGSO8mYI/cHJQ+9FbN21aa+DrgNLnwObmyeSC8Rmpg==", + "dependencies": { + "@octokit/types": "^6.0.3", + "deprecation": "^2.0.0", + "once": "^1.4.0" + } + }, + "node_modules/@octokit/request/node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@octokit/rest": { + "version": "18.12.0", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-18.12.0.tgz", + "integrity": "sha512-gDPiOHlyGavxr72y0guQEhLsemgVjwRePayJ+FcKc2SJqKUbxbkvf5kAZEWA/MKvsfYlQAMVzNJE3ezQcxMJ2Q==", + "dependencies": { + "@octokit/core": "^3.5.1", + "@octokit/plugin-paginate-rest": "^2.16.8", + "@octokit/plugin-request-log": "^1.0.4", + "@octokit/plugin-rest-endpoint-methods": "^5.12.0" + } + }, + "node_modules/@octokit/types": { + "version": "6.41.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-6.41.0.tgz", + "integrity": "sha512-eJ2jbzjdijiL3B4PrSQaSjuF2sPEQPVCPzBvTHJD9Nz+9dw2SGH4K4xeQJ77YfTq5bRQ+bD8wT11JbeDPmxmGg==", + "dependencies": { + "@octokit/openapi-types": "^12.11.0" + } + }, "node_modules/@opentelemetry/api": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.4.1.tgz", @@ -1773,6 +1920,11 @@ ], "optional": true }, + "node_modules/before-after-hook": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.2.3.tgz", + "integrity": "sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==" + }, "node_modules/big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -2384,6 +2536,11 @@ "node": ">=0.4.0" } }, + "node_modules/deprecation": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/deprecation/-/deprecation-2.3.1.tgz", + "integrity": "sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==" + }, "node_modules/detect-libc": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", @@ -4611,7 +4768,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -6098,6 +6254,11 @@ "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==", "dev": true }, + "node_modules/universal-user-agent": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-6.0.0.tgz", + "integrity": "sha512-isyNax3wXoKaulPDZWHQqbmIx1k2tb9fb3GGDBRxCscfYV2Ch7WxPArBsFEG8s/safwXTT7H4QGhaIkTp9447w==" + }, "node_modules/universalify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", @@ -6633,8 +6794,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/ws": { "version": "8.13.0", diff --git a/package.json b/package.json index c3eeb9f80d..f3c6b6149d 100644 --- a/package.json +++ b/package.json @@ -516,12 +516,12 @@ }, { "command": "vscode-docker.registries.deployImageToAzure", - "when": "view == dockerRegistries && viewItem =~ /(DockerV2|DockerHubV2);Tag;/ && isAzureAccountInstalled", + "when": "view == dockerRegistries && viewItem =~ /commontag/i", "group": "regs_tag_1_general@4" }, { "command": "vscode-docker.registries.deployImageToAca", - "when": "view == dockerRegistries && viewItem =~ /(DockerV2|DockerHubV2|GitLabV4);Tag;/ && isAzureAccountInstalled", + "when": "view == dockerRegistries && viewItem =~ /commontag/i", "group": "regs_tag_1_general@6" }, { @@ -3030,10 +3030,10 @@ "@azure/arm-containerregistry": "^10.1.0", "@azure/storage-blob": "^12.14.0", "@microsoft/compose-language-service": "^0.2.0", - "@microsoft/vscode-azext-azureappservice": "^1.0.2", + "@microsoft/vscode-azext-azureappservice": "^2.2.0", "@microsoft/vscode-azext-azureauth": "^1.1.2", - "@microsoft/vscode-azext-azureutils": "^1.1.5", - "@microsoft/vscode-azext-utils": "^1.2.2", + "@microsoft/vscode-azext-azureutils": "^2.0.1", + "@microsoft/vscode-azext-utils": "^2.0.1", "@microsoft/vscode-container-client": "^0.1.0", "@microsoft/vscode-docker-registries": "file:../vscode-docker-extensibility/packages/vscode-docker-registries/microsoft-vscode-docker-registries-0.0.1-alpha.tgz", "dayjs": "^1.11.7", diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 641055ba06..c7403f0be3 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -54,6 +54,8 @@ import { registerWorkspaceCommand } from "./registerWorkspaceCommand"; import { createAzureRegistry } from "./registries/azure/createAzureRegistry"; import { deleteAzureRegistry } from "./registries/azure/deleteAzureRegistry"; import { deleteAzureRepository } from "./registries/azure/deleteAzureRepository"; +import { deployImageToAca } from "./registries/azure/deployImageToAca"; +import { deployImageToAzure } from "./registries/azure/deployImageToAzure"; import { openInAzurePortal } from "./registries/azure/openInAzurePortal"; import { buildImageInAzure } from "./registries/azure/tasks/buildImageInAzure"; import { runAzureTask } from "./registries/azure/tasks/runAzureTask"; @@ -164,8 +166,8 @@ export function registerCommands(): void { registerCommand('vscode-docker.registries.copyImageDigest', copyRemoteImageDigest); registerCommand('vscode-docker.registries.copyRemoteFullTag', copyRemoteFullTag); // registerCommand('vscode-docker.registries.deleteImage', deleteRemoteImage); - // registerCommand('vscode-docker.registries.deployImageToAzure', deployImageToAzure); - // registerCommand('vscode-docker.registries.deployImageToAca', deployImageToAca); + registerCommand('vscode-docker.registries.deployImageToAzure', deployImageToAzure); + registerCommand('vscode-docker.registries.deployImageToAca', deployImageToAca); registerCommand('vscode-docker.registries.disconnectRegistry', disconnectRegistry); registerCommand('vscode-docker.registries.help', registryHelp); registerWorkspaceCommand('vscode-docker.registries.logInToDockerCli', logInToDockerCli); diff --git a/src/commands/registries/azure/DockerAssignAcrPullRoleStep.ts b/src/commands/registries/azure/DockerAssignAcrPullRoleStep.ts index 74095300da..0761162134 100644 --- a/src/commands/registries/azure/DockerAssignAcrPullRoleStep.ts +++ b/src/commands/registries/azure/DockerAssignAcrPullRoleStep.ts @@ -1,89 +1,91 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (c) Microsoft Corporation. All rights reserved. -// * Licensed under the MIT License. See LICENSE.md in the project root for license information. -// *--------------------------------------------------------------------------------------------*/ - -// import type { IAppServiceWizardContext } from "@microsoft/vscode-azext-azureappservice"; // These are only dev-time imports so don't need to be lazy -// import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; -// import { randomUUID } from "crypto"; -// import { l10n, Progress } from "vscode"; -// import { ext } from "../../../extensionVariables"; -// import { AzureRegistryTreeItem } from '../../../tree/registries/azure/AzureRegistryTreeItem'; -// import { RemoteTagTreeItem } from '../../../tree/registries/RemoteTagTreeItem'; -// import { getArmAuth, getArmContainerRegistry, getAzExtAppService, getAzExtAzureUtils } from "../../../utils/lazyPackages"; - -// export class DockerAssignAcrPullRoleStep extends AzureWizardExecuteStep { -// public priority: number = 141; // execute after DockerSiteCreateStep - -// public constructor(private readonly tagTreeItem: RemoteTagTreeItem) { -// super(); -// } - -// public async execute(context: IAppServiceWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise { -// const message: string = l10n.t('Granting permission for App Service to pull image from ACR...'); -// ext.outputChannel.info(message); -// progress.report({ message: message }); - -// const azExtAzureUtils = await getAzExtAzureUtils(); -// const vscAzureAppService = await getAzExtAppService(); -// const armAuth = await getArmAuth(); -// const armContainerRegistry = await getArmContainerRegistry(); -// const authClient = azExtAzureUtils.createAzureClient(context, armAuth.AuthorizationManagementClient); -// const crmClient = azExtAzureUtils.createAzureClient(context, armContainerRegistry.ContainerRegistryManagementClient); -// const appSvcClient = await vscAzureAppService.createWebSiteClient(context); - -// // If we're in `execute`, then `shouldExecute` passed and `this.tagTreeItem.parent.parent` is guaranteed to be an AzureRegistryTreeItem -// const registryTreeItem: AzureRegistryTreeItem = this.tagTreeItem.parent.parent as unknown as AzureRegistryTreeItem; - -// // 1. Get the registry resource. We will need the ID. -// const registry = await crmClient.registries.get(registryTreeItem.resourceGroup, registryTreeItem.registryName); - -// if (!(registry?.id)) { -// throw new Error( -// l10n.t('Unable to get details from Container Registry {0}', registryTreeItem.baseUrl) -// ); -// } - -// // 2. Get the role definition for the AcrPull role. We will need the definition ID. This role is built-in and should always exist. -// const acrPullRoleDefinition = (await azExtAzureUtils.uiUtils.listAllIterator(authClient.roleDefinitions.list(registry.id, { filter: `roleName eq 'AcrPull'` })))[0]; - -// if (!(acrPullRoleDefinition?.id)) { -// throw new Error( -// l10n.t('Unable to get AcrPull role definition on subscription {0}', context.subscriptionId) -// ); -// } - -// // 3. Get the info for the now-created web site. We will need the principal ID. -// const siteInfo = await appSvcClient.webApps.get(context.site.resourceGroup, context.site.name); - -// if (!(siteInfo?.identity?.principalId)) { -// throw new Error( -// l10n.t('Unable to get identity principal ID for web site {0}', context.site.name) -// ); -// } - -// // 4. On the registry, assign the AcrPull role to the principal representing the website -// await authClient.roleAssignments.create(registry.id, randomUUID(), { -// principalId: siteInfo.identity.principalId, -// roleDefinitionId: acrPullRoleDefinition.id, -// principalType: 'ServicePrincipal', -// }); - -// // 5. Set the web app to use the desired ACR image, which was not done in DockerSiteCreateStep. Get the config and then update it. -// const config = await appSvcClient.webApps.getConfiguration(context.site.resourceGroup, context.site.name); - -// if (!config) { -// throw new Error( -// l10n.t('Unable to get configuration for web site {0}', context.site.name) -// ); -// } - -// config.linuxFxVersion = `DOCKER|${this.tagTreeItem.fullTag}`; -// await appSvcClient.webApps.updateConfiguration(context.site.resourceGroup, context.site.name, config); -// } - -// public shouldExecute(context: IAppServiceWizardContext): boolean { -// return !!(context.site) && !!(this.tagTreeItem?.parent?.parent) && this.tagTreeItem.parent.parent instanceof AzureRegistryTreeItem -// && !context.customLocation; -// } -// } +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { IAppServiceWizardContext } from "@microsoft/vscode-azext-azureappservice"; // These are only dev-time imports so don't need to be lazy +import { AzureWizardExecuteStep } from "@microsoft/vscode-azext-utils"; +import { CommonTag } from "@microsoft/vscode-docker-registries"; +import { randomUUID } from "crypto"; +import { Progress, l10n } from "vscode"; +import { ext } from "../../../extensionVariables"; +import { AzureRegistry, isAzureTagItem } from "../../../tree/registries/Azure/AzureRegistryDataProvider"; +import { UnifiedRegistryItem } from "../../../tree/registries/UnifiedRegistryTreeDataProvider"; +import { getFullImageNameFromRegistryTagItem, getResourceGroupFromAzureRegistryItem } from "../../../tree/registries/registryTreeUtils"; +import { getArmAuth, getArmContainerRegistry, getAzExtAppService, getAzExtAzureUtils } from "../../../utils/lazyPackages"; + +export class DockerAssignAcrPullRoleStep extends AzureWizardExecuteStep { + public priority: number = 141; // execute after DockerSiteCreateStep + + public constructor(private readonly tagTreeItem: UnifiedRegistryItem) { + super(); + } + + public async execute(context: IAppServiceWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise { + const message: string = l10n.t('Granting permission for App Service to pull image from ACR...'); + ext.outputChannel.info(message); + progress.report({ message: message }); + + const azExtAzureUtils = await getAzExtAzureUtils(); + const vscAzureAppService = await getAzExtAppService(); + const armAuth = await getArmAuth(); + const armContainerRegistry = await getArmContainerRegistry(); + const authClient = azExtAzureUtils.createAzureClient(context, armAuth.AuthorizationManagementClient); + const crmClient = azExtAzureUtils.createAzureClient(context, armContainerRegistry.ContainerRegistryManagementClient); + const appSvcClient = await vscAzureAppService.createWebSiteClient(context); + + // If we're in `execute`, then `shouldExecute` passed and `this.tagTreeItem.parent.parent` is guaranteed to be an AzureRegistryTreeItem + const registryTreeItem: UnifiedRegistryItem = this.tagTreeItem.parent.parent as unknown as UnifiedRegistryItem; + + // 1. Get the registry resource. We will need the ID. + const registry = await crmClient.registries.get(getResourceGroupFromAzureRegistryItem(registryTreeItem.wrappedItem), registryTreeItem.wrappedItem.label); + + if (!(registry?.id)) { + throw new Error( + l10n.t('Unable to get details from Container Registry {0}', registryTreeItem.wrappedItem.baseUrl) + ); + } + + // 2. Get the role definition for the AcrPull role. We will need the definition ID. This role is built-in and should always exist. + const acrPullRoleDefinition = (await azExtAzureUtils.uiUtils.listAllIterator(authClient.roleDefinitions.list(registry.id, { filter: `roleName eq 'AcrPull'` })))[0]; + + if (!(acrPullRoleDefinition?.id)) { + throw new Error( + l10n.t('Unable to get AcrPull role definition on subscription {0}', context.subscriptionId) + ); + } + + // 3. Get the info for the now-created web site. We will need the principal ID. + const siteInfo = await appSvcClient.webApps.get(context.site.resourceGroup, context.site.name); + + if (!(siteInfo?.identity?.principalId)) { + throw new Error( + l10n.t('Unable to get identity principal ID for web site {0}', context.site.name) + ); + } + + // 4. On the registry, assign the AcrPull role to the principal representing the website + await authClient.roleAssignments.create(registry.id, randomUUID(), { + principalId: siteInfo.identity.principalId, + roleDefinitionId: acrPullRoleDefinition.id, + principalType: 'ServicePrincipal', + }); + + // 5. Set the web app to use the desired ACR image, which was not done in DockerSiteCreateStep. Get the config and then update it. + const config = await appSvcClient.webApps.getConfiguration(context.site.resourceGroup, context.site.name); + + if (!config) { + throw new Error( + l10n.t('Unable to get configuration for web site {0}', context.site.name) + ); + } + + const fullTag = getFullImageNameFromRegistryTagItem(this.tagTreeItem.wrappedItem); + config.linuxFxVersion = `DOCKER|${fullTag}`; + await appSvcClient.webApps.updateConfiguration(context.site.resourceGroup, context.site.name, config); + } + + public shouldExecute(context: IAppServiceWizardContext): boolean { + return !!(context.site) && isAzureTagItem(this.tagTreeItem.wrappedItem) && !context.customLocation; + } +} diff --git a/src/commands/registries/azure/DockerSiteCreateStep.ts b/src/commands/registries/azure/DockerSiteCreateStep.ts index 6d05b1f474..38131a36f0 100644 --- a/src/commands/registries/azure/DockerSiteCreateStep.ts +++ b/src/commands/registries/azure/DockerSiteCreateStep.ts @@ -1,138 +1,137 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (c) Microsoft Corporation. All rights reserved. -// * Licensed under the MIT License. See LICENSE.md in the project root for license information. -// *--------------------------------------------------------------------------------------------*/ - -// import type { NameValuePair, Site, SiteConfig, WebSiteManagementClient } from '@azure/arm-appservice'; // These are only dev-time imports so don't need to be lazy -// import type { CustomLocation } from "@microsoft/vscode-azext-azureappservice"; // These are only dev-time imports so don't need to be lazy -// import type { AzExtLocation } from '@microsoft/vscode-azext-azureutils'; // These are only dev-time imports so don't need to be lazy -// import { AzureWizardExecuteStep, nonNullProp, nonNullValueAndProp } from "@microsoft/vscode-azext-utils"; -// import { Progress, l10n } from "vscode"; -// import { ext } from "../../../extensionVariables"; -// import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; -// import { getAzExtAppService, getAzExtAzureUtils } from '../../../utils/lazyPackages'; -// import { IAppServiceContainerWizardContext } from './deployImageToAzure'; - -// export class DockerSiteCreateStep extends AzureWizardExecuteStep { -// public priority: number = 140; - -// public constructor(private readonly node: UnifiedRegistryItem) { -// super(); -// } - -// public async execute(context: IAppServiceContainerWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise { -// const creatingNewApp: string = l10n.t('Creating web app "{0}"...', context.newSiteName); -// ext.outputChannel.info(creatingNewApp); -// progress.report({ message: creatingNewApp }); -// const siteConfig = await this.getNewSiteConfig(context); - -// const azExtAzureUtils = await getAzExtAzureUtils(); -// const vscAzureAppService = await getAzExtAppService(); - -// const location: AzExtLocation = await azExtAzureUtils.LocationListStep.getLocation(context); -// const locationName: string = nonNullProp(location, 'name'); - -// const client: WebSiteManagementClient = await vscAzureAppService.createWebSiteClient(context); -// const siteEnvelope: Site = { -// name: context.newSiteName, -// location: locationName, -// serverFarmId: nonNullValueAndProp(context.plan, 'id'), -// siteConfig: siteConfig -// }; - -// if (context.customLocation) { -// // deploying to Azure Arc -// siteEnvelope.kind = 'app,linux,kubernetes,container'; -// await this.addCustomLocationProperties(siteEnvelope, context.customLocation); -// } else { -// siteEnvelope.identity = { -// type: 'SystemAssigned' -// }; -// } - -// context.site = await client.webApps.beginCreateOrUpdateAndWait(nonNullValueAndProp(context.resourceGroup, 'name'), nonNullProp(context, 'newSiteName'), siteEnvelope); -// } - -// private async getNewSiteConfig(context: IAppServiceContainerWizardContext): Promise { -// const registryTI: UnifiedRegistryItem = this.node.parent.parent; - -// let username: string | undefined; -// let password: string | undefined; -// let registryUrl: string | undefined; -// const appSettings: NameValuePair[] = []; - -// // Scenarios: -// // ACR -> App Service, NOT Arc App Service. Use managed service identity. -// if (registryTI instanceof AzureRegistryTreeItem && !context.customLocation) { -// appSettings.push({ name: 'DOCKER_ENABLE_CI', value: 'true' }); - -// // Don't need an image, username, or password--just create an empty web app to assign permissions and then configure with an image -// // `DockerAssignAcrPullRoleStep` handles it after that -// return { -// acrUseManagedIdentityCreds: true, -// appSettings -// }; -// } -// // ACR -> Arc App Service. Use regular auth. Same as any V2 registry but different way of getting auth. -// else if (registryTI instanceof AzureRegistryTreeItem && context.customLocation) { -// const cred = await registryTI.tryGetAdminCredentials(context); -// if (!cred?.username || !cred?.passwords?.[0]?.value) { -// throw new Error(l10n.t('Azure App service deployment on Azure Arc only supports running images from Azure Container Registries with admin enabled')); -// } - -// username = cred.username; -// password = cred.passwords[0].value; -// registryUrl = registryTI.baseUrl; -// } -// // Docker Hub -> App Service *OR* Arc App Service -// else if (registryTI instanceof DockerHubNamespaceTreeItem) { -// username = registryTI.parent.username; -// password = await registryTI.parent.getPassword(); -// registryUrl = 'https://index.docker.io'; -// } -// // Generic registry -> App Service *OR* Arc App Service -// else if (registryTI instanceof DockerV2RegistryTreeItemBase) { -// if (registryTI instanceof GenericDockerV2RegistryTreeItem) { -// username = registryTI.cachedProvider.username; -// password = await getRegistryPassword(registryTI.cachedProvider); -// } else { -// throw new RangeError(l10n.t('Unrecognized node type "{0}"', registryTI.constructor.name)); -// } - -// registryUrl = registryTI.baseUrl; -// } else { -// throw new RangeError(l10n.t('Unrecognized node type "{0}"', registryTI.constructor.name)); -// } - -// if (username && password) { -// appSettings.push({ name: "DOCKER_REGISTRY_SERVER_USERNAME", value: username }); -// appSettings.push({ name: "DOCKER_REGISTRY_SERVER_PASSWORD", value: password }); -// } - -// if (registryUrl) { -// appSettings.push({ name: 'DOCKER_REGISTRY_SERVER_URL', value: registryUrl }); -// } - -// if (context.webSitesPort) { -// appSettings.push({ name: "WEBSITES_PORT", value: context.webSitesPort.toString() }); -// } - -// const linuxFxVersion = `DOCKER|${this.node.fullTag}`; -// TODO: review this later -// const linuxFxVersion = ''; -// return { -// linuxFxVersion, -// appSettings -// }; -// } - -// private addCustomLocationProperties(site: Site, customLocation: CustomLocation): void { -// site.extendedLocation = { name: customLocation.id, type: 'customLocation' }; -// } - -// public shouldExecute(context: IAppServiceContainerWizardContext): boolean { -// return !context.site; -// } -// } - -// TODO: review this later +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { NameValuePair, Site, SiteConfig, WebSiteManagementClient } from '@azure/arm-appservice'; // These are only dev-time imports so don't need to be lazy +import type { CustomLocation } from "@microsoft/vscode-azext-azureappservice"; // These are only dev-time imports so don't need to be lazy +import type { AzExtLocation } from '@microsoft/vscode-azext-azureutils'; // These are only dev-time imports so don't need to be lazy +import { AzureWizardExecuteStep, nonNullProp, nonNullValueAndProp } from "@microsoft/vscode-azext-utils"; +import { CommonRegistry, CommonTag, isDockerHubRegistryItem, isGenericV2Registry } from '@microsoft/vscode-docker-registries'; +import { Progress, l10n } from "vscode"; +import { ext } from "../../../extensionVariables"; +import { AzureRegistryDataProvider, isAzureRegistryItem } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { getFullImageNameFromRegistryTagItem } from '../../../tree/registries/registryTreeUtils'; +import { getAzExtAppService, getAzExtAzureUtils } from '../../../utils/lazyPackages'; +import { IAppServiceContainerWizardContext } from './deployImageToAzure'; + +export class DockerSiteCreateStep extends AzureWizardExecuteStep { + public priority: number = 140; + + public constructor(private readonly tagItem: UnifiedRegistryItem) { + super(); + } + + public async execute(context: IAppServiceContainerWizardContext, progress: Progress<{ message?: string; increment?: number }>): Promise { + const creatingNewApp: string = l10n.t('Creating web app "{0}"...', context.newSiteName); + ext.outputChannel.info(creatingNewApp); + progress.report({ message: creatingNewApp }); + const siteConfig = await this.getNewSiteConfig(context); + + const azExtAzureUtils = await getAzExtAzureUtils(); + const vscAzureAppService = await getAzExtAppService(); + + const location: AzExtLocation = await azExtAzureUtils.LocationListStep.getLocation(context); + const locationName: string = nonNullProp(location, 'name'); + + const client: WebSiteManagementClient = await vscAzureAppService.createWebSiteClient(context); + const siteEnvelope: Site = { + name: context.newSiteName, + location: locationName, + serverFarmId: nonNullValueAndProp(context.plan, 'id'), + siteConfig: siteConfig + }; + + if (context.customLocation) { + // deploying to Azure Arc + siteEnvelope.kind = 'app,linux,kubernetes,container'; + this.addCustomLocationProperties(siteEnvelope, context.customLocation); + } else { + siteEnvelope.identity = { + type: 'SystemAssigned' + }; + } + + context.site = await client.webApps.beginCreateOrUpdateAndWait(nonNullValueAndProp(context.resourceGroup, 'name'), nonNullProp(context, 'newSiteName'), siteEnvelope); + } + + private async getNewSiteConfig(context: IAppServiceContainerWizardContext): Promise { + const registryTI: UnifiedRegistryItem = this.tagItem.parent.parent as unknown as UnifiedRegistryItem; + + let username: string | undefined; + let password: string | undefined; + let registryUrl: string | undefined; + const appSettings: NameValuePair[] = []; + + // Scenarios: + // ACR -> App Service, NOT Arc App Service. Use managed service identity. + if (isAzureRegistryItem(registryTI.wrappedItem) && !context.customLocation) { + appSettings.push({ name: 'DOCKER_ENABLE_CI', value: 'true' }); + + // Don't need an image, username, or password--just create an empty web app to assign permissions and then configure with an image + // `DockerAssignAcrPullRoleStep` handles it after that + return { + acrUseManagedIdentityCreds: true, + appSettings + }; + } + // ACR -> Arc App Service. Use regular auth. Same as any V2 registry but different way of getting auth. + else if (isAzureRegistryItem(registryTI.wrappedItem) && context.customLocation) { + const cred = await (registryTI.provider as unknown as AzureRegistryDataProvider).tryGetAdminCredentials(registryTI.wrappedItem); + if (!cred?.username || !cred?.passwords?.[0]?.value) { + throw new Error(l10n.t('Azure App service deployment on Azure Arc only supports running images from Azure Container Registries with admin enabled')); + } + + username = cred.username; + password = cred.passwords[0].value; + registryUrl = registryTI.wrappedItem.baseUrl.toString(); + } + // Docker Hub -> App Service *OR* Arc App Service + else if (isDockerHubRegistryItem(registryTI.wrappedItem)) { + const loginInformation = await registryTI.provider.getLoginInformation(registryTI.wrappedItem); + username = loginInformation.username; + password = loginInformation.secret; + registryUrl = 'https://index.docker.io'; + } + // Generic registry -> App Service *OR* Arc App Service + else if (isGenericV2Registry(registryTI.wrappedItem)) { + const loginInformation = await registryTI.provider.getLoginInformation(registryTI.wrappedItem); + username = loginInformation.username; + password = loginInformation.secret; + registryUrl = (registryTI.wrappedItem as CommonRegistry).baseUrl.toString(); + } + // TODO: add case for GitHub Container Registry + else { + throw new RangeError(l10n.t('Unrecognized node type "{0}"', registryTI.constructor.name)); + } + + if (username && password) { + appSettings.push({ name: "DOCKER_REGISTRY_SERVER_USERNAME", value: username }); + appSettings.push({ name: "DOCKER_REGISTRY_SERVER_PASSWORD", value: password }); + } + + if (registryUrl) { + appSettings.push({ name: 'DOCKER_REGISTRY_SERVER_URL', value: registryUrl }); + } + + if (context.webSitesPort) { + appSettings.push({ name: "WEBSITES_PORT", value: context.webSitesPort.toString() }); + } + + const linuxFxVersion = `DOCKER|${getFullImageNameFromRegistryTagItem(this.tagItem.wrappedItem)}`; + + return { + linuxFxVersion, + appSettings + }; + } + + private addCustomLocationProperties(site: Site, customLocation: CustomLocation): void { + site.extendedLocation = { name: customLocation.id, type: 'customLocation' }; + } + + public shouldExecute(context: IAppServiceContainerWizardContext): boolean { + return !context.site; + } +} diff --git a/src/commands/registries/azure/DockerWebhookCreateStep.ts b/src/commands/registries/azure/DockerWebhookCreateStep.ts index 25f56c23d1..5c48b185dd 100644 --- a/src/commands/registries/azure/DockerWebhookCreateStep.ts +++ b/src/commands/registries/azure/DockerWebhookCreateStep.ts @@ -1,96 +1,99 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (c) Microsoft Corporation. All rights reserved. -// * Licensed under the MIT License. See LICENSE.md in the project root for license information. -// *--------------------------------------------------------------------------------------------*/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ -// import type { Site } from '@azure/arm-appservice'; // These are only dev-time imports so don't need to be lazy -// import type { Webhook, WebhookCreateParameters } from '@azure/arm-containerregistry'; // These are only dev-time imports so don't need to be lazy -// import type { IAppServiceWizardContext } from "@microsoft/vscode-azext-azureappservice"; // These are only dev-time imports so don't need to be lazy -// import { AzureWizardExecuteStep, nonNullProp } from "@microsoft/vscode-azext-utils"; -// import * as vscode from "vscode"; -// import { ext } from "../../../extensionVariables"; -// import { AzureRegistryTreeItem } from '../../../tree/registries/azure/AzureRegistryTreeItem'; -// import { AzureRepositoryTreeItem } from '../../../tree/registries/azure/AzureRepositoryTreeItem'; -// import { DockerHubRepositoryTreeItem } from '../../../tree/registries/dockerHub/DockerHubRepositoryTreeItem'; -// import { RemoteTagTreeItem } from '../../../tree/registries/RemoteTagTreeItem'; -// import { cryptoUtils } from '../../../utils/cryptoUtils'; -// import { getArmContainerRegistry, getAzExtAppService, getAzExtAzureUtils } from "../../../utils/lazyPackages"; +import type { Site } from '@azure/arm-appservice'; // These are only dev-time imports so don't need to be lazy +import type { Webhook, WebhookCreateParameters } from '@azure/arm-containerregistry'; // These are only dev-time imports so don't need to be lazy +import type { IAppServiceWizardContext } from "@microsoft/vscode-azext-azureappservice"; // These are only dev-time imports so don't need to be lazy +import { AzureWizardExecuteStep, nonNullProp } from "@microsoft/vscode-azext-utils"; +import { CommonRepository, CommonTag, isDockerHubRepositoryItem } from '@microsoft/vscode-docker-registries'; +import * as vscode from "vscode"; +import { ext } from "../../../extensionVariables"; +import { AzureRegistry, isAzureRepositoryItem } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { getResourceGroupFromAzureRegistryItem } from '../../../tree/registries/registryTreeUtils'; +import { cryptoUtils } from '../../../utils/cryptoUtils'; +import { getArmContainerRegistry, getAzExtAppService, getAzExtAzureUtils } from "../../../utils/lazyPackages"; -// export class DockerWebhookCreateStep extends AzureWizardExecuteStep { -// public priority: number = 142; // execute after DockerAssignAcrPullRoleStep -// private _treeItem: RemoteTagTreeItem; -// public constructor(treeItem: RemoteTagTreeItem) { -// super(); -// this._treeItem = treeItem; -// } +export class DockerWebhookCreateStep extends AzureWizardExecuteStep { + public priority: number = 142; // execute after DockerAssignAcrPullRoleStep -// public async execute(context: IAppServiceWizardContext, progress: vscode.Progress<{ -// message?: string; -// increment?: number; -// }>): Promise { -// const vscAzureAppService = await getAzExtAppService(); -// vscAzureAppService.registerAppServiceExtensionVariables(ext); -// const site: Site = nonNullProp(context, 'site'); -// const parsedSite = new vscAzureAppService.ParsedSite(site, context); -// const siteClient = await parsedSite.createClient(context); -// const appUri: string = (await siteClient.getWebAppPublishCredential()).scmUri; -// if (this._treeItem.parent instanceof AzureRepositoryTreeItem) { -// const creatingNewWebhook: string = vscode.l10n.t('Creating webhook for web app "{0}"...', context.newSiteName); -// ext.outputChannel.info(creatingNewWebhook); -// progress.report({ message: creatingNewWebhook }); -// const webhook = await this.createWebhookForApp(context, this._treeItem, context.site, appUri); -// ext.outputChannel.info(vscode.l10n.t('Created webhook "{0}" with scope "{1}", id: "{2}" and location: "{3}"', webhook.name, webhook.scope, webhook.id, webhook.location)); -// } else if (this._treeItem.parent instanceof DockerHubRepositoryTreeItem) { -// // point to dockerhub to create a webhook -// // http://cloud.docker.com/repository/docker///webHooks -// const dockerhubPrompt: string = vscode.l10n.t('Copy & Open'); -// const dockerhubUri: string = `https://cloud.docker.com/repository/docker/${this._treeItem.parent.parent.namespace}/${this._treeItem.parent.repoName}/webHooks`; + public constructor(private readonly tagItem: UnifiedRegistryItem) { + super(); + } -// // NOTE: The response to the information message is not awaited but handled independently of the wizard steps. -// // VS Code will hide such messages in the notifications pane after a period of time; awaiting them risks -// // the user never noticing them in the first place, which means the wizard would never complete, and the -// // user left with the impression that the action never completes. + public async execute(context: IAppServiceWizardContext, progress: vscode.Progress<{ + message?: string; + increment?: number; + }>): Promise { + const vscAzureAppService = await getAzExtAppService(); + vscAzureAppService.registerAppServiceExtensionVariables(ext); + const site: Site = nonNullProp(context, 'site'); + const parsedSite = new vscAzureAppService.ParsedSite(site, context); + const siteClient = await parsedSite.createClient(context); + const appUri: string = (await siteClient.getWebAppPublishCredential()).scmUri; -// /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ -// vscode.window -// .showInformationMessage(vscode.l10n.t('To set up a CI/CD webhook, open the page "{0}" and enter the URI to the created web app in your dockerhub account', dockerhubUri), dockerhubPrompt) -// .then(response => { -// if (response) { -// void vscode.env.clipboard.writeText(appUri); -// void vscode.env.openExternal(vscode.Uri.parse(dockerhubUri)); -// } -// }); -// } -// } + if (isAzureRepositoryItem(this.tagItem.parent.wrappedItem)) { + const creatingNewWebhook: string = vscode.l10n.t('Creating webhook for web app "{0}"...', context.newSiteName); + ext.outputChannel.info(creatingNewWebhook); + progress.report({ message: creatingNewWebhook }); + const webhook = await this.createWebhookForApp(context, context.site, appUri); + ext.outputChannel.info(vscode.l10n.t('Created webhook "{0}" with scope "{1}", id: "{2}" and location: "{3}"', webhook.name, webhook.scope, webhook.id, webhook.location)); + } else if (isDockerHubRepositoryItem(this.tagItem.parent.wrappedItem)) { + const registryName = this.tagItem.parent.parent.wrappedItem.label; + const repoName = (this.tagItem.parent as unknown as CommonRepository).wrappedItem.label; + // point to dockerhub to create a webhook + // http://cloud.docker.com/repository/docker///webHooks + const dockerhubPrompt: string = vscode.l10n.t('Copy & Open'); + const dockerhubUri: string = `https://cloud.docker.com/repository/docker/${registryName}/${repoName}/webHooks`; -// public shouldExecute(context: IAppServiceWizardContext): boolean { -// return !!context.site && (this._treeItem.parent instanceof AzureRepositoryTreeItem || this._treeItem.parent instanceof DockerHubRepositoryTreeItem); -// } + // NOTE: The response to the information message is not awaited but handled independently of the wizard steps. + // VS Code will hide such messages in the notifications pane after a period of time; awaiting them risks + // the user never noticing them in the first place, which means the wizard would never complete, and the + // user left with the impression that the action never completes. -// private async createWebhookForApp(context: IAppServiceWizardContext, node: RemoteTagTreeItem, site: Site, appUri: string): Promise { -// const maxLength: number = 50; -// const numRandomChars: number = 6; + /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ + vscode.window + .showInformationMessage(vscode.l10n.t('To set up a CI/CD webhook, open the page "{0}" and enter the URI to the created web app in your dockerhub account', dockerhubUri), dockerhubPrompt) + .then(response => { + if (response) { + void vscode.env.clipboard.writeText(appUri); + void vscode.env.openExternal(vscode.Uri.parse(dockerhubUri)); + } + }); + } + } -// let webhookName: string = site.name; -// // remove disallowed characters -// webhookName = webhookName.replace(/[^a-zA-Z0-9]/g, ''); -// // trim to max length -// webhookName = webhookName.substr(0, maxLength - numRandomChars); -// // add random chars for uniqueness and to ensure min length is met -// webhookName += cryptoUtils.getRandomHexString(numRandomChars); + public shouldExecute(context: IAppServiceWizardContext): boolean { + return !!context.site && (isAzureRepositoryItem(this.tagItem.parent.wrappedItem) || isDockerHubRepositoryItem(this.tagItem.parent.wrappedItem)); + } -// // variables derived from the container registry -// const registryTreeItem: AzureRegistryTreeItem = (node.parent).parent; -// const armContainerRegistry = await getArmContainerRegistry(); -// const azExtAzureUtils = await getAzExtAzureUtils(); -// const crmClient = azExtAzureUtils.createAzureClient(context, armContainerRegistry.ContainerRegistryManagementClient); -// const webhookCreateParameters: WebhookCreateParameters = { -// location: registryTreeItem.registryLocation, -// serviceUri: appUri, -// scope: `${node.parent.repoName}:${node.tag}`, -// actions: ["push"], -// status: 'enabled' -// }; -// return await crmClient.webhooks.beginCreateAndWait(registryTreeItem.resourceGroup, registryTreeItem.registryName, webhookName, webhookCreateParameters); -// } -// } + private async createWebhookForApp(context: IAppServiceWizardContext, site: Site, appUri: string): Promise { + const maxLength: number = 50; + const numRandomChars: number = 6; + + let webhookName: string = site.name; + // remove disallowed characters + webhookName = webhookName.replace(/[^a-zA-Z0-9]/g, ''); + // trim to max length + webhookName = webhookName.substr(0, maxLength - numRandomChars); + // add random chars for uniqueness and to ensure min length is met + webhookName += cryptoUtils.getRandomHexString(numRandomChars); + + // variables derived from the container registry + const registryTreeItem: UnifiedRegistryItem = this.tagItem.parent.parent as unknown as UnifiedRegistryItem; + const armContainerRegistry = await getArmContainerRegistry(); + const azExtAzureUtils = await getAzExtAzureUtils(); + const crmClient = azExtAzureUtils.createAzureClient(context, armContainerRegistry.ContainerRegistryManagementClient); + const webhookCreateParameters: WebhookCreateParameters = { + location: registryTreeItem.wrappedItem.registryProperties.location, + serviceUri: appUri, + scope: `${this.tagItem.parent.wrappedItem.label}:${this.tagItem.wrappedItem.label}`, + actions: ["push"], + status: 'enabled' + }; + const resourceGroup = getResourceGroupFromAzureRegistryItem(registryTreeItem.wrappedItem); + return await crmClient.webhooks.beginCreateAndWait(resourceGroup, registryTreeItem.wrappedItem.label, webhookName, webhookCreateParameters); + } +} diff --git a/src/commands/registries/azure/WebSitesPortPromptStep.ts b/src/commands/registries/azure/WebSitesPortPromptStep.ts index e16ca26dab..48667641c0 100644 --- a/src/commands/registries/azure/WebSitesPortPromptStep.ts +++ b/src/commands/registries/azure/WebSitesPortPromptStep.ts @@ -1,35 +1,35 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (c) Microsoft Corporation. All rights reserved. -// * Licensed under the MIT License. See LICENSE.md in the project root for license information. -// *--------------------------------------------------------------------------------------------*/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ -// import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; -// import { l10n } from 'vscode'; -// import { IAppServiceContainerWizardContext } from './deployImageToAzure'; +import { AzureWizardPromptStep } from "@microsoft/vscode-azext-utils"; +import { l10n } from 'vscode'; +import { IAppServiceContainerWizardContext } from './deployImageToAzure'; -// export class WebSitesPortPromptStep extends AzureWizardPromptStep { +export class WebSitesPortPromptStep extends AzureWizardPromptStep { -// public async prompt(context: IAppServiceContainerWizardContext): Promise { -// const prompt: string = l10n.t('What port does your app listen on?'); -// const placeHolder: string = '80'; -// const value: string = '80'; -// const portString: string = await context.ui.showInputBox({ prompt, placeHolder, value, validateInput }); -// context.webSitesPort = parseInt(portString, 10); -// } + public async prompt(context: IAppServiceContainerWizardContext): Promise { + const prompt: string = l10n.t('What port does your app listen on?'); + const placeHolder: string = '80'; + const value: string = '80'; + const portString: string = await context.ui.showInputBox({ prompt, placeHolder, value, validateInput }); + context.webSitesPort = parseInt(portString, 10); + } -// public shouldPrompt(context: IAppServiceContainerWizardContext): boolean { -// return !!context.customLocation; -// } -// } + public shouldPrompt(context: IAppServiceContainerWizardContext): boolean { + return !!context.customLocation; + } +} -// function validateInput(value: string | undefined): string | undefined { -// if (Number(value)) { -// const port: number = parseInt(value, 10); -// if (port >= 1 && port <= 65535) { -// return undefined; -// } -// } +function validateInput(value: string | undefined): string | undefined { + if (Number(value)) { + const port: number = parseInt(value, 10); + if (port >= 1 && port <= 65535) { + return undefined; + } + } -// return l10n.t('Port must be a positive integer (1 to 65535).'); -// } + return l10n.t('Port must be a positive integer (1 to 65535).'); +} diff --git a/src/commands/registries/azure/deployImageToAca.ts b/src/commands/registries/azure/deployImageToAca.ts index da4d9a495a..a2e0332a49 100644 --- a/src/commands/registries/azure/deployImageToAca.ts +++ b/src/commands/registries/azure/deployImageToAca.ts @@ -1,94 +1,98 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (c) Microsoft Corporation. All rights reserved. -// * Licensed under the MIT License. See LICENSE.md in the project root for license information. -// *--------------------------------------------------------------------------------------------*/ - -// import { contextValueExperience, IActionContext, UserCancelledError } from '@microsoft/vscode-azext-utils'; -// import * as semver from 'semver'; -// import * as vscode from 'vscode'; -// import { ext } from '../../../extensionVariables'; -// import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; -// import { installExtension } from '../../../utils/installExtension'; -// import { addImageTaggingTelemetry } from '../../images/tagImage'; - -// const acaExtensionId = 'ms-azuretools.vscode-azurecontainerapps'; -// const minimumAcaExtensionVersion = '0.4.0'; - -// // The interface of the command options passed to the Azure Container Apps extension's deployImageToAca command -// interface DeployImageToAcaOptionsContract { -// image: string; -// registryName: string; -// username?: string; -// secret?: string; -// } - -// export async function deployImageToAca(context: IActionContext, node?: UnifiedRegistryItem): Promise { -// // Assert installation of the ACA extension -// if (!isAcaExtensionInstalled()) { -// // This will always throw a `UserCancelledError` but with the appropriate step name -// // based on user choice about installation -// await openAcaInstallPage(context); -// } - -// if (!node) { -// node = await contextValueExperience(context, ext.registriesRoot, { include: 'azurecontainerregistry' }); -// } - -// const commandOptions: Partial = { -// // image: node.fullTag, -// }; - -// addImageTaggingTelemetry(context, commandOptions.image, ''); - -// // const registry: RegistryTreeItemBase = node.parent.parent; -// // if (registry instanceof AzureRegistryTreeItem) { -// // // No additional work to do; ACA can handle this on its own -// // } else { -// // const { auth } = await registry.getDockerCliCredentials() as { auth?: { username?: string, password?: string } }; - -// // if (!auth?.username || !auth?.password) { -// // throw new Error(vscode.l10n.t('No credentials found for registry "{0}".', registry.label)); -// // } - -// // if (registry instanceof DockerHubNamespaceTreeItem) { -// // // Ensure Docker Hub images are prefixed with 'docker.io/...' -// // if (!/^docker\.io\//i.test(commandOptions.image)) { -// // commandOptions.image = 'docker.io/' + commandOptions.image; -// // } -// // } - -// // commandOptions.username = auth.username; -// // commandOptions.secret = auth.password; -// // } - -// // commandOptions.registryName = nonNullProp(parseDockerLikeImageName(commandOptions.image), 'registry'); - -// // // Don't wait -// // void vscode.commands.executeCommand('containerApps.deployImageApi', commandOptions); -// // TODO: review this later -// } - -// function isAcaExtensionInstalled(): boolean { -// const acaExtension = vscode.extensions.getExtension(acaExtensionId); - -// if (!acaExtension?.packageJSON?.version) { -// // If the ACA extension is not present, or the package JSON didn't come through, or the version is not present, then it's not installed -// return false; -// } - -// const acaVersion = semver.coerce(acaExtension.packageJSON.version); -// const minVersion = semver.coerce(minimumAcaExtensionVersion); - -// return semver.gte(acaVersion, minVersion); -// } - -// async function openAcaInstallPage(context: IActionContext): Promise { -// const message = vscode.l10n.t( -// 'Version {0} or higher of the Azure Container Apps extension is required to deploy to Azure Container Apps. Would you like to install it now?', -// minimumAcaExtensionVersion -// ); - -// await installExtension(context, acaExtensionId, message); - -// throw new UserCancelledError('installAcaExtensionAccepted'); -// } +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { contextValueExperience, IActionContext, nonNullProp, UserCancelledError } from '@microsoft/vscode-azext-utils'; +import { parseDockerLikeImageName } from '@microsoft/vscode-container-client'; +import { CommonRegistry, CommonTag, isDockerHubRegistryItem, LoginInformation } from '@microsoft/vscode-docker-registries'; +import * as semver from 'semver'; +import * as vscode from 'vscode'; +import { ext } from '../../../extensionVariables'; +import { isAzureRegistryItem } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { getFullImageNameFromRegistryTagItem } from '../../../tree/registries/registryTreeUtils'; +import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { installExtension } from '../../../utils/installExtension'; +import { addImageTaggingTelemetry } from '../../images/tagImage'; + +const acaExtensionId = 'ms-azuretools.vscode-azurecontainerapps'; +const minimumAcaExtensionVersion = '0.4.0'; + +// The interface of the command options passed to the Azure Container Apps extension's deployImageToAca command +interface DeployImageToAcaOptionsContract { + image: string; + registryName: string; + username?: string; + secret?: string; +} + +export async function deployImageToAca(context: IActionContext, node?: UnifiedRegistryItem): Promise { + // Assert installation of the ACA extension + if (!isAcaExtensionInstalled()) { + // This will always throw a `UserCancelledError` but with the appropriate step name + // based on user choice about installation + await openAcaInstallPage(context); + } + + if (!node) { + node = await contextValueExperience(context, ext.registriesTree, { include: 'commontag' }); + } + + const commandOptions: Partial = { + image: getFullImageNameFromRegistryTagItem(node.wrappedItem), + }; + + addImageTaggingTelemetry(context, commandOptions.image, ''); + + const registry: UnifiedRegistryItem = node.parent.parent as unknown as UnifiedRegistryItem; + + if (isAzureRegistryItem(registry.wrappedItem)) { + // No additional work to do; ACA can handle this on its own + } else { + const logInInfo: LoginInformation = await registry.provider.getLoginInformation(registry.wrappedItem); + + if (!logInInfo?.username || !logInInfo?.secret) { + throw new Error(vscode.l10n.t('No credentials found for registry "{0}".', registry.wrappedItem.label)); + } + + if (isDockerHubRegistryItem(registry.wrappedItem)) { + // Ensure Docker Hub images are prefixed with 'docker.io/...' + if (!/^docker\.io\//i.test(commandOptions.image)) { + commandOptions.image = 'docker.io/' + commandOptions.image; + } + } + + commandOptions.username = logInInfo.username; + commandOptions.secret = logInInfo.secret; + } + + commandOptions.registryName = nonNullProp(parseDockerLikeImageName(commandOptions.image), 'registry'); + + // Don't wait + void vscode.commands.executeCommand('containerApps.deployImageApi', commandOptions); +} + +function isAcaExtensionInstalled(): boolean { + const acaExtension = vscode.extensions.getExtension(acaExtensionId); + + if (!acaExtension?.packageJSON?.version) { + // If the ACA extension is not present, or the package JSON didn't come through, or the version is not present, then it's not installed + return false; + } + + const acaVersion = semver.coerce(acaExtension.packageJSON.version); + const minVersion = semver.coerce(minimumAcaExtensionVersion); + + return semver.gte(acaVersion, minVersion); +} + +async function openAcaInstallPage(context: IActionContext): Promise { + const message = vscode.l10n.t( + 'Version {0} or higher of the Azure Container Apps extension is required to deploy to Azure Container Apps. Would you like to install it now?', + minimumAcaExtensionVersion + ); + + await installExtension(context, acaExtensionId, message); + + throw new UserCancelledError('installAcaExtensionAccepted'); +} diff --git a/src/commands/registries/azure/deployImageToAzure.ts b/src/commands/registries/azure/deployImageToAzure.ts index 375982c4ea..cbed25dcf4 100644 --- a/src/commands/registries/azure/deployImageToAzure.ts +++ b/src/commands/registries/azure/deployImageToAzure.ts @@ -1,80 +1,76 @@ -// /*--------------------------------------------------------------------------------------------- -// * Copyright (c) Microsoft Corporation. All rights reserved. -// * Licensed under the MIT License. See LICENSE.md in the project root for license information. -// *--------------------------------------------------------------------------------------------*/ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ -// import type { Site } from '@azure/arm-appservice'; // These are only dev-time imports so don't need to be lazy -// import type { IAppServiceWizardContext } from "@microsoft/vscode-azext-azureappservice"; // These are only dev-time imports so don't need to be lazy -// import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, IActionContext, nonNullProp } from "@microsoft/vscode-azext-utils"; -// import { env, l10n, Uri, window } from "vscode"; -// import { ext } from "../../../extensionVariables"; -// import { RegistryApi } from '../../../tree/registries/all/RegistryApi'; -// import { azureRegistryProviderId } from '../../../tree/registries/azure/azureRegistryProvider'; -// import { registryExpectedContextValues } from '../../../tree/registries/registryContextValues'; -// import { RemoteTagTreeItem } from '../../../tree/registries/RemoteTagTreeItem'; -// import { getAzActTreeItem, getAzExtAppService, getAzExtAzureUtils } from '../../../utils/lazyPackages'; -// import { DockerAssignAcrPullRoleStep } from './DockerAssignAcrPullRoleStep'; -// import { DockerSiteCreateStep } from './DockerSiteCreateStep'; -// import { DockerWebhookCreateStep } from './DockerWebhookCreateStep'; -// import { WebSitesPortPromptStep } from './WebSitesPortPromptStep'; +import type { Site } from '@azure/arm-appservice'; // These are only dev-time imports so don't need to be lazy +import type { IAppServiceWizardContext } from "@microsoft/vscode-azext-azureappservice"; // These are only dev-time imports so don't need to be lazy +import { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, IActionContext, contextValueExperience, createSubscriptionContext, nonNullProp } from "@microsoft/vscode-azext-utils"; +import { CommonTag } from '@microsoft/vscode-docker-registries'; +import { Uri, env, l10n, window } from "vscode"; +import { ext } from "../../../extensionVariables"; +import { AzureSubscriptionRegistryItem } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { getAzExtAppService, getAzExtAzureUtils } from '../../../utils/lazyPackages'; +import { registryExperience } from '../../../utils/registryExperience'; +import { DockerAssignAcrPullRoleStep } from './DockerAssignAcrPullRoleStep'; +import { DockerSiteCreateStep } from './DockerSiteCreateStep'; +import { DockerWebhookCreateStep } from './DockerWebhookCreateStep'; +import { WebSitesPortPromptStep } from './WebSitesPortPromptStep'; +export interface IAppServiceContainerWizardContext extends IAppServiceWizardContext { + webSitesPort?: number; +} -// export interface IAppServiceContainerWizardContext extends IAppServiceWizardContext { -// webSitesPort?: number; -// } +export async function deployImageToAzure(context: IActionContext, node?: UnifiedRegistryItem): Promise { + if (!node) { + node = await contextValueExperience(context, ext.registriesTree, { include: 'commontag' }); + } -// export async function deployImageToAzure(context: IActionContext, node?: RemoteTagTreeItem): Promise { -// if (!node) { -// node = await ext.registriesTree.showTreeItemPicker([registryExpectedContextValues.dockerHub.tag, registryExpectedContextValues.dockerV2.tag], context); -// } + const azExtAzureUtils = await getAzExtAzureUtils(); + const vscAzureAppService = await getAzExtAppService(); -// const azExtAzureUtils = await getAzExtAzureUtils(); -// const vscAzureAppService = await getAzExtAppService(); -// const azActTreeItem = await getAzActTreeItem(); + const promptSteps: AzureWizardPromptStep[] = []; -// const wizardContext: IActionContext & Partial = { -// ...context, -// newSiteOS: vscAzureAppService.WebsiteOS.linux, -// newSiteKind: vscAzureAppService.AppKind.app -// }; -// const promptSteps: AzureWizardPromptStep[] = []; -// // Create a temporary azure account tree item since Azure might not be connected -// const azureAccountTreeItem = new azActTreeItem.AzureAccountTreeItem(ext.registriesRoot, { id: azureRegistryProviderId, api: RegistryApi.DockerV2 }); -// const subscriptionStep = await azureAccountTreeItem.getSubscriptionPromptStep(wizardContext); -// if (subscriptionStep) { -// promptSteps.push(subscriptionStep); -// } + const subscriptionItem = await registryExperience(context, ext.azureRegistryDataProvider, { include: 'azuresubscription' }) as AzureSubscriptionRegistryItem; + const subscriptionContext = createSubscriptionContext(subscriptionItem.subscription); + const wizardContext: IActionContext & Partial = { + ...context, + ...subscriptionContext, + newSiteOS: vscAzureAppService.WebsiteOS.linux, + newSiteKind: vscAzureAppService.AppKind.app + }; -// promptSteps.push(new vscAzureAppService.SiteNameStep()); -// promptSteps.push(new azExtAzureUtils.ResourceGroupListStep()); -// vscAzureAppService.CustomLocationListStep.addStep(wizardContext, promptSteps); -// promptSteps.push(new WebSitesPortPromptStep()); -// promptSteps.push(new vscAzureAppService.AppServicePlanListStep()); + promptSteps.push(new vscAzureAppService.SiteNameStep()); + promptSteps.push(new azExtAzureUtils.ResourceGroupListStep()); + vscAzureAppService.CustomLocationListStep.addStep(wizardContext, promptSteps); + promptSteps.push(new WebSitesPortPromptStep()); + promptSteps.push(new vscAzureAppService.AppServicePlanListStep()); -// // Get site config before running the wizard so that any problems with the tag tree item are shown at the beginning of the process -// const executeSteps: AzureWizardExecuteStep[] = [ -// new DockerSiteCreateStep(node), -// new DockerAssignAcrPullRoleStep(node), -// new DockerWebhookCreateStep(node), -// ]; + // Get site config before running the wizard so that any problems with the tag tree item are shown at the beginning of the process + const executeSteps: AzureWizardExecuteStep[] = [ + new DockerSiteCreateStep(node), + new DockerAssignAcrPullRoleStep(node), + new DockerWebhookCreateStep(node), + ]; -// const title = l10n.t('Create new web app'); -// const wizard = new AzureWizard(wizardContext, { title, promptSteps, executeSteps }); -// await wizard.prompt(); -// await wizard.execute(); + const title = l10n.t('Create new web app'); + const wizard = new AzureWizard(wizardContext, { title, promptSteps, executeSteps }); + await wizard.prompt(); + await wizard.execute(); -// const site: Site = nonNullProp(wizardContext, 'site'); -// const siteUri: string = `https://${site.defaultHostName}`; -// const createdNewWebApp: string = l10n.t('Successfully created web app "{0}": {1}', site.name, siteUri); -// ext.outputChannel.info(createdNewWebApp); + const site: Site = nonNullProp(wizardContext, 'site'); + const siteUri: string = `https://${site.defaultHostName}`; + const createdNewWebApp: string = l10n.t('Successfully created web app "{0}": {1}', site.name, siteUri); + ext.outputChannel.info(createdNewWebApp); -// const openSite: string = l10n.t('Open Site'); -// // don't wait -// /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ -// window.showInformationMessage(createdNewWebApp, ...[openSite]).then((selection) => { -// if (selection === openSite) { -// /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ -// env.openExternal(Uri.parse(siteUri)); -// } -// }); -// } + const openSite: string = l10n.t('Open Site'); + // don't wait + /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ + window.showInformationMessage(createdNewWebApp, ...[openSite]).then((selection) => { + if (selection === openSite) { + /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ + env.openExternal(Uri.parse(siteUri)); + } + }); +} diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index 93c3fad0db..2f42ce0a33 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -14,6 +14,7 @@ import { ContainersTreeItem } from './tree/containers/ContainersTreeItem'; import { ContextsTreeItem } from './tree/contexts/ContextsTreeItem'; import { ImagesTreeItem } from './tree/images/ImagesTreeItem'; import { NetworksTreeItem } from './tree/networks/NetworksTreeItem'; +import { AzureRegistryDataProvider } from './tree/registries/Azure/AzureRegistryDataProvider'; import { UnifiedRegistryItem, UnifiedRegistryTreeDataProvider } from './tree/registries/UnifiedRegistryTreeDataProvider'; import { VolumesTreeItem } from './tree/volumes/VolumesTreeItem'; import { AzExtLogOutputChannelWrapper } from './utils/AzExtLogOutputChannelWrapper'; @@ -50,6 +51,7 @@ export namespace ext { export let registriesTreeView: TreeView>; export let registriesRoot: UnifiedRegistryTreeDataProvider; export let genericRegistryV2DataProvider: GenericRegistryV2DataProvider; + export let azureRegistryDataProvider: AzureRegistryDataProvider; export let volumesTree: AzExtTreeDataProvider; export let volumesTreeView: TreeView; diff --git a/src/tree/registerTrees.ts b/src/tree/registerTrees.ts index e55991df7f..2280a11905 100644 --- a/src/tree/registerTrees.ts +++ b/src/tree/registerTrees.ts @@ -46,14 +46,16 @@ export function registerTrees(): void { const urtdp = new UnifiedRegistryTreeDataProvider(ext.context.globalState); const genericRegistryV2DataProvider = new GenericRegistryV2DataProvider(ext.context); + const azureRegistryDataProvider = new AzureRegistryDataProvider(ext.context); urtdp.registerProvider(new GitHubRegistryDataProvider(ext.context)); urtdp.registerProvider(new DockerHubRegistryDataProvider(ext.context)); - urtdp.registerProvider(new AzureRegistryDataProvider(ext.context)); + urtdp.registerProvider(azureRegistryDataProvider); urtdp.registerProvider(genericRegistryV2DataProvider); ext.registriesRoot = urtdp; ext.registriesTreeView = vscode.window.createTreeView('dockerRegistries', { treeDataProvider: urtdp }); ext.registriesTree = urtdp; ext.genericRegistryV2DataProvider = genericRegistryV2DataProvider; + ext.azureRegistryDataProvider = azureRegistryDataProvider; ext.volumesRoot = new VolumesTreeItem(undefined); const volumesLoadMore = 'vscode-docker.volumes.loadMore'; diff --git a/src/tree/registries/Azure/AzureRegistryDataProvider.ts b/src/tree/registries/Azure/AzureRegistryDataProvider.ts index f2e5cdaf7a..2da1ca14f1 100644 --- a/src/tree/registries/Azure/AzureRegistryDataProvider.ts +++ b/src/tree/registries/Azure/AzureRegistryDataProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See LICENSE in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { Registry as AcrRegistry } from '@azure/arm-containerregistry'; +import type { Registry as AcrRegistry, RegistryListCredentialsResult } from '@azure/arm-containerregistry'; import { AzureSubscription, VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; import { RegistryV2DataProvider, V2Registry, V2RegistryItem, V2Repository, V2Tag, registryV2Request } from '@microsoft/vscode-docker-registries'; import { CommonRegistryItem, isRegistryRoot } from '@microsoft/vscode-docker-registries/lib/clients/Common/models'; @@ -37,6 +37,14 @@ export function isAzureRegistryItem(item: unknown): item is AzureRegistry { return !!item && typeof item === 'object' && (item as AzureRegistryItem).additionalContextValues?.includes('azureContainerRegistry'); } +export function isAzureRepositoryItem(item: unknown): item is AzureRepository { + return !!item && typeof item === 'object' && (item as AzureRepository).additionalContextValues?.includes('azureContainerRepository'); +} + +export function isAzureTagItem(item: unknown): item is AzureTag { + return !!item && typeof item === 'object' && (item as AzureTag).additionalContextValues?.includes('azureContainerTag'); +} + export class AzureRegistryDataProvider extends RegistryV2DataProvider implements vscode.Disposable { public readonly id = 'vscode-docker.azureContainerRegistry'; public readonly label = vscode.l10n.t('Azure'); @@ -190,6 +198,15 @@ export class AzureRegistryDataProvider extends RegistryV2DataProvider implements } } + public async tryGetAdminCredentials(azureRegistry: AzureRegistry): Promise { + if (azureRegistry.registryProperties.adminUserEnabled) { + const client = await createAzureContainerRegistryClient(azureRegistry.subscription); + return await client.registries.listCredentials(getResourceGroupFromId(azureRegistry.id), azureRegistry.label); + } else { + return undefined; + } + } + protected override getAuthenticationProvider(item: AzureRegistryItem): ACROAuthProvider { const registryString = item.baseUrl.toString(); diff --git a/src/tree/registries/registryTreeUtils.ts b/src/tree/registries/registryTreeUtils.ts index 683ac1f33e..6c8fc98acc 100644 --- a/src/tree/registries/registryTreeUtils.ts +++ b/src/tree/registries/registryTreeUtils.ts @@ -5,6 +5,8 @@ import { CommonRegistry, CommonRepository, CommonTag, isRegistry, isRepository, isTag } from "@microsoft/vscode-docker-registries"; import { l10n } from "vscode"; +import { getResourceGroupFromId } from "../../utils/azureUtils"; +import { AzureRegistryItem } from "./Azure/AzureRegistryDataProvider"; export function getImageNameFromRegistryTagItem(tag: CommonTag): string { if (!isTag(tag) || !isRepository(tag.parent)) { @@ -37,3 +39,11 @@ export function getFullImageNameFromRegistryTagItem(tag: CommonTag): string { const baseImagePath = getBaseImagePathFromRegistryItem(tag.parent.parent); return `${baseImagePath}/${imageName}`; } + +export function getResourceGroupFromAzureRegistryItem(node: AzureRegistryItem): string { + if (!isRegistry(node)) { + throw new Error('Unable to get resource group'); + } + + return getResourceGroupFromId(node.id); +} diff --git a/src/utils/azureUtils.ts b/src/utils/azureUtils.ts index dbcc5ae66f..8d23588739 100644 --- a/src/utils/azureUtils.ts +++ b/src/utils/azureUtils.ts @@ -5,13 +5,7 @@ import type { ContainerRegistryManagementClient } from '@azure/arm-containerregistry'; import { AzureSubscription } from '@microsoft/vscode-azext-azureauth'; -import { ISubscriptionContext } from '@microsoft/vscode-azext-utils'; -import { Request } from 'node-fetch'; -import { URLSearchParams } from 'url'; import { l10n } from 'vscode'; -import { httpRequest, RequestOptionsLike } from './httpRequest'; - -const refreshTokens: { [key: string]: string } = {}; function parseResourceId(id: string): RegExpMatchArray { const matches: RegExpMatchArray | null = id.match(/\/subscriptions\/(.*)\/resourceGroups\/(.*)\/providers\/(.*)\/(.*)/i); @@ -25,53 +19,6 @@ export function getResourceGroupFromId(id: string): string { return parseResourceId(id)[2]; } -/* eslint-disable @typescript-eslint/naming-convention */ -export async function acquireAcrAccessToken(registryHost: string, subContext: ISubscriptionContext, scope: string): Promise { - const options: RequestOptionsLike = { - form: { - grant_type: 'refresh_token', - service: registryHost, - scope: scope, - refresh_token: undefined - }, - method: 'POST', - }; - - try { - if (refreshTokens[registryHost]) { - options.form.refresh_token = refreshTokens[registryHost]; - const responseFromCachedToken = await httpRequest<{ access_token: string }>(`https://${registryHost}/oauth2/token`, options); - return (await responseFromCachedToken.json()).access_token; - } - } catch { /* No-op, fall back to a new refresh token */ } - - options.form.refresh_token = refreshTokens[registryHost] = await acquireAcrRefreshToken(registryHost, subContext); - const response = await httpRequest<{ access_token: string }>(`https://${registryHost}/oauth2/token`, options); - return (await response.json()).access_token; -} - -export async function acquireAcrRefreshToken(registryHost: string, subContext: ISubscriptionContext): Promise { - const options: RequestOptionsLike = { - method: 'POST', - form: { - grant_type: 'access_token', - service: registryHost, - tenant: subContext.tenantId, - }, - }; - - const response = await httpRequest<{ refresh_token: string }>(`https://${registryHost}/oauth2/exchange`, options, async (request) => { - // Obnoxiously, the oauth2/exchange endpoint wants the token in the form data's access_token field, so we need to pick it off the signed auth header and move it there - await subContext.credentials.signRequest(request); - const token = (request.headers.get('authorization') as string).replace(/Bearer\s+/i, ''); - - const formData = new URLSearchParams({ ...options.form, access_token: token }); - return new Request(request.url, { method: 'POST', body: formData }); - }); - - return (await response.json()).refresh_token; -} - export async function createAzureContainerRegistryClient(subscriptionItem: AzureSubscription): Promise { return new (await import('@azure/arm-containerregistry')).ContainerRegistryManagementClient(subscriptionItem.credential, subscriptionItem.subscriptionId); } diff --git a/src/utils/registryExperience.ts b/src/utils/registryExperience.ts new file mode 100644 index 0000000000..25a8d5d5dc --- /dev/null +++ b/src/utils/registryExperience.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, ContextValueFilter, IActionContext, QuickPickWizardContext, RecursiveQuickPickStep, runQuickPickWizard } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; + +export async function registryExperience(context: IActionContext, tdp: vscode.TreeDataProvider, contextValueFilter: ContextValueFilter): Promise { + const promptSteps: AzureWizardPromptStep[] = [ + new RecursiveQuickPickStep( + tdp, + { + contextValueFilter: contextValueFilter, + skipIfOne: true + } + ) + ]; + + return await runQuickPickWizard(context, { + hideStepCount: true, + promptSteps: promptSteps, + }); +}