diff --git a/package-lock.json b/package-lock.json index a95cfdef1d..38a8e790ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,10 +13,12 @@ "@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-azureutils": "^1.1.5", - "@microsoft/vscode-azext-utils": "^1.2.2", + "@microsoft/vscode-azext-azureappservice": "~2.0", + "@microsoft/vscode-azext-azureauth": "^1.1.2", + "@microsoft/vscode-azext-azureutils": "^2.0.0", + "@microsoft/vscode-azext-utils": "^2.1.1", "@microsoft/vscode-container-client": "^0.1.0", + "@microsoft/vscode-docker-registries": "^0.1.0", "dayjs": "^1.11.7", "dockerfile-language-server-nodejs": "^0.10.2", "fs-extra": "^11.1.1", @@ -259,6 +261,23 @@ "node": ">=14.0.0" } }, + "node_modules/@azure/arm-subscriptions": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@azure/arm-subscriptions/-/arm-subscriptions-5.1.0.tgz", + "integrity": "sha512-6BeOF2eQWNLq22ch7xP9RxYnPjtGev54OUCGggKOWoOvmesK7jUZbIyLk8JeXDT21PEl7iyYnxw78gxJ7zBxQw==", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "@azure/core-auth": "^1.3.0", + "@azure/core-client": "^1.6.1", + "@azure/core-lro": "^2.2.0", + "@azure/core-paging": "^1.2.0", + "@azure/core-rest-pipeline": "^1.8.0", + "tslib": "^2.2.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@azure/core-auth": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.4.0.tgz", @@ -405,8 +424,7 @@ "node_modules/@azure/ms-rest-azure-env": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@azure/ms-rest-azure-env/-/ms-rest-azure-env-2.0.0.tgz", - "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==", - "peer": true + "integrity": "sha512-dG76W7ElfLi+fbTjnZVGj+M9e0BIEJmRxU6fHaUQ12bZBe8EJKYb2GV50YWNaP2uJiVQ5+7nXEVj1VN1UQtaEw==" }, "node_modules/@azure/storage-blob": { "version": "12.14.0", @@ -663,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.0.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureappservice/-/vscode-azext-azureappservice-2.0.0.tgz", + "integrity": "sha512-evtnwjKZk6wBGXJQy3sNEEFdGztQAT09HeuyU4qpFwWRBIoPE1wg0TO7S7psc+N5W8sFiWBy3h15mOlU6daocA==", "dependencies": { "@azure/abort-controller": "^1.0.4", "@azure/arm-appinsights": "^5.0.0-beta.4", @@ -676,8 +694,8 @@ "@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.0", + "@microsoft/vscode-azext-utils": "^2.0.0", "dayjs": "^1.11.2", "fs-extra": "^10.0.0", "globby": "^11.0.2", @@ -689,7 +707,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": { @@ -706,18 +724,27 @@ } }, "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": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-azureauth/-/vscode-azext-azureauth-1.1.2.tgz", + "integrity": "sha512-cacmiEv1GiofOKaQzgRqXrDqPoFc3HAGiaer+ENNrdeW6elks0o5Z0oXkTIauS26yeYQUUaooB6DezvQU6LC+Q==", + "dependencies": { + "@azure/arm-subscriptions": "^5.1.0", + "@azure/ms-rest-azure-env": "^2.0.0" } }, "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", @@ -727,7 +754,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" }, @@ -744,9 +771,9 @@ } }, "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.1.1", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-azext-utils/-/vscode-azext-utils-2.1.1.tgz", + "integrity": "sha512-MUrBET1YzS0QmpMY9ZcRjZe9/U0vmGtYI9hRTMB7NYBtutUF19+Y7W6k6G7Odlnn3MjRIrc8ZMmumvsVqlxhPg==", "dependencies": { "@microsoft/vscode-azureresources-api": "^2.0.4", "@vscode/extension-telemetry": "^0.6.2", @@ -784,6 +811,15 @@ "tree-kill": "^1.2.2" } }, + "node_modules/@microsoft/vscode-docker-registries": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@microsoft/vscode-docker-registries/-/vscode-docker-registries-0.1.0.tgz", + "integrity": "sha512-d9LXqxs0JDDz0RnORfPy6ouECEA3ujxdOiXYUjysP8qaADnFfaVeOmDJylAFtwEqlz3UwmXDp+SRaOLk10BViQ==", + "dependencies": { + "dayjs": "^1.11.7", + "node-fetch": "^2.6.11" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4498,9 +4534,9 @@ "optional": true }, "node_modules/node-fetch": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.9.tgz", - "integrity": "sha512-DJm/CJkZkRjKKj4Zi4BsKVZh3ValV5IR5s7LVZnW+6YMh0W1BfNA8XSs6DLMGYlId5F3KnA70uu2qepcR08Qqg==", + "version": "2.6.12", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.12.tgz", + "integrity": "sha512-C/fGU2E8ToujUivIO0H+tpQ6HWo4eEmchoPIoXtxCrVghxdKq+QOHqEZW7tuP3KlV3bC8FRMO5nMCC7Zm1VP6g==", "dependencies": { "whatwg-url": "^5.0.0" }, diff --git a/package.json b/package.json index 413bd1048e..0363c6513c 100644 --- a/package.json +++ b/package.json @@ -122,10 +122,6 @@ "command": "vscode-docker.registries.azure.buildImage", "when": "isWorkspaceTrusted" }, - { - "command": "vscode-docker.registries.azure.runFileAsTask", - "when": "isWorkspaceTrusted" - }, { "command": "vscode-docker.help.openWalkthrough", "when": "never" @@ -145,19 +141,18 @@ { "command": "vscode-docker.containers.group.remove", "when": "never" + }, + { + "command": "vscode-docker.activateRegistryProviders", + "when": "never" } ], "editor/context": [ { - "when": "isWorkspaceTrusted && editorLangId == dockerfile && isAzureAccountInstalled", + "when": "isWorkspaceTrusted && editorLangId == dockerfile", "command": "vscode-docker.registries.azure.buildImage", "group": "docker" }, - { - "when": "isWorkspaceTrusted && editorLangId == yaml && isAzureAccountInstalled", - "command": "vscode-docker.registries.azure.runFileAsTask", - "group": "docker" - }, { "when": "isWorkspaceTrusted && editorLangId == dockercompose", "command": "vscode-docker.compose.down", @@ -186,15 +181,10 @@ ], "explorer/context": [ { - "when": "isWorkspaceTrusted && resourceFilename =~ /dockerfile/i && isAzureAccountInstalled", + "when": "isWorkspaceTrusted && resourceFilename =~ /dockerfile/i", "command": "vscode-docker.registries.azure.buildImage", "group": "docker" }, - { - "when": "isWorkspaceTrusted && resourceLangId == yaml && isAzureAccountInstalled", - "command": "vscode-docker.registries.azure.runFileAsTask", - "group": "docker" - }, { "when": "isWorkspaceTrusted && resourceLangId == dockercompose", "command": "vscode-docker.compose.down", @@ -464,11 +454,6 @@ "when": "view == dockerRegistries && viewItem == azure;DockerV2;RegistryProvider;", "group": "inline" }, - { - "command": "vscode-docker.registries.azure.viewTaskLogs", - "when": "view == dockerRegistries && viewItem == azureTaskRun", - "group": "inline" - }, { "command": "vscode-docker.networks.inspect", "when": "view == dockerNetworks && viewItem =~ /network$/i", @@ -481,102 +466,92 @@ }, { "command": "vscode-docker.registries.azure.createRegistry", - "when": "view == dockerRegistries && viewItem == azureextensionui.azureSubscription", + "when": "view == dockerRegistries && viewItem =~ /azuresubscription/i", "group": "regs_1_general@1" }, { "command": "vscode-docker.registries.azure.deleteRegistry", - "when": "view == dockerRegistries && viewItem == azure;DockerV2;Registry;", + "when": "view == dockerRegistries && viewItem =~ /azure;.*commonregistry/i", "group": "regs_reg_2_destructive@1" }, { "command": "vscode-docker.registries.pullRepository", - "when": "view == dockerRegistries && viewItem =~ /Repository;/", + "when": "view == dockerRegistries && viewItem =~ /commonrepository/", "group": "regs_repo_1_general@1" }, { "command": "vscode-docker.registries.azure.deleteRepository", - "when": "view == dockerRegistries && viewItem == azure;DockerV2;Repository;", + "when": "view == dockerRegistries && viewItem =~ /azure;.*commonrepository/i", "group": "regs_repo_2_destructive@1" }, { "command": "vscode-docker.registries.pullImage", - "when": "view == dockerRegistries && viewItem =~ /Tag;/", + "when": "view == dockerRegistries && viewItem =~ /commontag/i", "group": "regs_tag_1_general@1" }, { "command": "vscode-docker.registries.copyRemoteFullTag", - "when": "view == dockerRegistries && viewItem =~ /(DockerV2|DockerHubV2);Tag;/", + "when": "view == dockerRegistries && viewItem =~ /commontag/i", "group": "regs_tag_1_general@2" }, { "command": "vscode-docker.registries.copyImageDigest", - "when": "view == dockerRegistries && viewItem =~ /DockerV2;Tag;/", + "when": "view == dockerRegistries && viewItem =~ /commontag/i && !(viewItem =~ /commontag;.*dockerhub/i)", "group": "regs_tag_1_general@3" }, { "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" }, { "command": "vscode-docker.registries.azure.untagImage", - "when": "view == dockerRegistries && viewItem == azure;DockerV2;Tag;", + "when": "view == dockerRegistries && viewItem =~ /azure;.*commontag/i", "group": "regs_tag_2_destructive@1" }, { "command": "vscode-docker.registries.deleteImage", - "when": "view == dockerRegistries && viewItem =~ /DockerV2;Tag;/", + "when": "view == dockerRegistries && viewItem =~ /commontag/i && !(viewItem =~ /commontag;.*(dockerhub|github)/i)", "group": "regs_tag_2_destructive@2" }, { - "command": "vscode-docker.registries.azure.runTask", - "when": "view == dockerRegistries && viewItem == azureTask", - "group": "regs_task_1_general@1" - }, - { - "command": "vscode-docker.registries.copyImageDigest", - "when": "view == dockerRegistries && viewItem == azureTaskRun", - "group": "regs_taskRun_1_general@1" - }, - { - "command": "vscode-docker.registries.azure.viewTaskLogs", - "when": "view == dockerRegistries && viewItem == azureTaskRun", - "group": "regs_taskRun_1_general@2" + "command": "vscode-docker.registries.disconnectRegistry", + "when": "view == dockerRegistries && viewItem =~ /commonregistryroot/i && !(viewItem =~ /commonregistryroot;.*generic/i)", + "group": "regs_yyy_destructive@1" }, { - "command": "vscode-docker.registries.disconnectRegistry", - "when": "view == dockerRegistries && viewItem =~ /RegistryProvider;/", + "command": "vscode-docker.registries.genericV2.removeTrackedRegistry", + "when": "view == dockerRegistries && viewItem =~ /commonregistry;.*generic/i", "group": "regs_yyy_destructive@1" }, { - "command": "vscode-docker.registries.disconnectRegistry", - "when": "view == dockerRegistries && viewItem == invalidRegistryProvider", + "command": "vscode-docker.registries.genericV2.addTrackedRegistry", + "when": "view == dockerRegistries && viewItem =~ /commonregistryroot;.*generic/i", "group": "regs_yyy_destructive@1" }, { "command": "vscode-docker.registries.azure.openInPortal", - "when": "view == dockerRegistries && viewItem =~ /azure(Subscription|;DockerV2;Registry;)/", + "when": "view == dockerRegistries && viewItem =~ /azuresubscription|azure;.*(commonregistry|commonrepository)/i", "group": "regs_zzz_common@1" }, { "command": "vscode-docker.registries.dockerHub.openInBrowser", - "when": "view == dockerRegistries && viewItem =~ /dockerHub;DockerHubV2;(Tag|Repository|Registry);/", + "when": "view == dockerRegistries && viewItem =~ /(commonregistry|commonrepository|commontag);.*dockerhub/i", "group": "regs_zzz_common@1" }, { "command": "vscode-docker.registries.azure.viewProperties", - "when": "view == dockerRegistries && viewItem =~ /azure(TaskRun|;DockerV2;Registry;)/", + "when": "view == dockerRegistries && viewItem =~ /azure;.*commonregistry/i", "group": "regs_zzz_common@2" }, { "command": "vscode-docker.registries.reconnectRegistry", - "when": "view == dockerRegistries && viewItem == registryConnectError", + "when": "view == dockerRegistries && viewItem =~ /registryConnectError/i", "group": "regs_zzz_common@8" }, { @@ -586,12 +561,7 @@ }, { "command": "vscode-docker.registries.refresh", - "when": "view == dockerRegistries && viewItem =~ /.*;.*;(Repository|Registry|RegistryProvider);/", - "group": "regs_zzz_common@9" - }, - { - "command": "vscode-docker.registries.refresh", - "when": "view == dockerRegistries && viewItem =~ /azure(Subscription|Tasks|Task|RunsWithoutTask)$/", + "when": "view == dockerRegistries && viewItem =~ /commonregistry|commonregistryroot|commonrepository/", "group": "regs_zzz_common@9" }, { @@ -2601,16 +2571,6 @@ "title": "%vscode-docker.commands.registries.azure.openInPortal%", "category": "%vscode-docker.commands.category.azureContainerRegistry%" }, - { - "command": "vscode-docker.registries.azure.runFileAsTask", - "title": "%vscode-docker.commands.registries.azure.runFileAsTask%", - "category": "%vscode-docker.commands.category.azureContainerRegistry%" - }, - { - "command": "vscode-docker.registries.azure.runTask", - "title": "%vscode-docker.commands.registries.azure.runTask%", - "category": "%vscode-docker.commands.category.azureContainerRegistry%" - }, { "command": "vscode-docker.registries.azure.selectSubscriptions", "title": "%vscode-docker.commands.registries.azure.selectSubscriptions%", @@ -2626,12 +2586,6 @@ "title": "%vscode-docker.commands.registries.azure.viewProperties%", "category": "%vscode-docker.commands.category.azureContainerRegistry%" }, - { - "command": "vscode-docker.registries.azure.viewTaskLogs", - "title": "%vscode-docker.commands.registries.azure.viewLogs%", - "category": "%vscode-docker.commands.category.azureContainerRegistry%", - "icon": "$(output)" - }, { "command": "vscode-docker.registries.connectRegistry", "title": "%vscode-docker.commands.registries.connect%", @@ -2668,6 +2622,16 @@ "title": "%vscode-docker.commands.registries.disconnectRegistry%", "category": "%vscode-docker.commands.category.dockerRegistries%" }, + { + "command": "vscode-docker.registries.genericV2.removeTrackedRegistry", + "title": "%vscode-docker.commands.registries.genericV2.removeTrackedRegistry%", + "category": "%vscode-docker.commands.category.dockerRegistries%" + }, + { + "command": "vscode-docker.registries.genericV2.addTrackedRegistry", + "title": "%vscode-docker.commands.registries.genericV2.addTrackedRegistry%", + "category": "%vscode-docker.commands.category.dockerRegistries%" + }, { "command": "vscode-docker.registries.dockerHub.openInBrowser", "title": "%vscode-docker.commands.registries.dockerHub.openInBrowser%", @@ -2787,6 +2751,11 @@ "title": "%vscode-docker.commands.contexts.help%", "category": "%vscode-docker.commands.category.contexts%", "icon": "$(question)" + }, + { + "command": "vscode-docker.activateRegistryProviders", + "title": "%vscode-docker.commands.activateRegistryProviders%", + "category": "%vscode-docker.commands.category.docker%" } ], "views": { @@ -3033,10 +3002,12 @@ "@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-azureutils": "^1.1.5", - "@microsoft/vscode-azext-utils": "^1.2.2", + "@microsoft/vscode-azext-azureappservice": "~2.0", + "@microsoft/vscode-azext-azureauth": "^1.1.2", + "@microsoft/vscode-azext-azureutils": "^2.0.0", + "@microsoft/vscode-azext-utils": "^2.1.1", "@microsoft/vscode-container-client": "^0.1.0", + "@microsoft/vscode-docker-registries": "^0.1.0", "dayjs": "^1.11.7", "dockerfile-language-server-nodejs": "^0.10.2", "fs-extra": "^11.1.1", diff --git a/package.nls.json b/package.nls.json index 51e38b7181..88dc4fe915 100644 --- a/package.nls.json +++ b/package.nls.json @@ -262,12 +262,9 @@ "vscode-docker.commands.registries.azure.deleteRegistry": "Delete Registry...", "vscode-docker.commands.registries.azure.deleteRepository": "Delete Repository...", "vscode-docker.commands.registries.azure.openInPortal": "Open in Portal", - "vscode-docker.commands.registries.azure.runFileAsTask": "Run as Task in Azure...", - "vscode-docker.commands.registries.azure.runTask": "Run Task", "vscode-docker.commands.registries.azure.selectSubscriptions": "Select Subscriptions...", "vscode-docker.commands.registries.azure.untagImage": "Untag Image...", "vscode-docker.commands.registries.azure.viewProperties": "View Properties", - "vscode-docker.commands.registries.azure.viewLogs": "View Logs", "vscode-docker.commands.registries.connect": "Connect Registry...", "vscode-docker.commands.registries.reconnectRegistry": "Re-enter credentials", "vscode-docker.commands.registries.copyImageDigest": "Copy Image Digest", @@ -276,6 +273,8 @@ "vscode-docker.commands.registries.deployImageToAzure": "Deploy Image to Azure App Service...", "vscode-docker.commands.registries.deployImageToAca": "Deploy Image to Azure Container Apps...", "vscode-docker.commands.registries.disconnectRegistry": "Disconnect", + "vscode-docker.commands.registries.genericV2.removeTrackedRegistry": "Disconnect from Generic Docker Registry", + "vscode-docker.commands.registries.genericV2.addTrackedRegistry": "Connect to Generic Docker Registry...", "vscode-docker.commands.registries.dockerHub.openInBrowser": "Open in Browser", "vscode-docker.commands.registries.help": "Registries Help", "vscode-docker.commands.registries.logInToDockerCli": "Log In to Docker CLI", @@ -294,6 +293,7 @@ "vscode-docker.commands.contexts.configureExplorer": "Configure Explorer...", "vscode-docker.commands.contexts.refresh": "Refresh", "vscode-docker.commands.contexts.help": "Docker Context Help", + "vscode-docker.commands.activateRegistryProviders": "Activate Registry Providers...", "vscode-docker.commands.category.docker": "Docker", "vscode-docker.commands.category.dockerContainers": "Docker Containers", "vscode-docker.commands.category.dockerImages": "Docker Images", diff --git a/src/DockerExtensionApi.ts b/src/DockerExtensionApi.ts index d623650664..082f590a74 100644 --- a/src/DockerExtensionApi.ts +++ b/src/DockerExtensionApi.ts @@ -3,9 +3,11 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { DockerExtensionApi as DockerExtensionRegistryApi, RegistryDataProvider, RegistryItem } from '@microsoft/vscode-docker-registries'; import * as vscode from 'vscode'; +import { ext } from './extensionVariables'; -export class DockerExtensionApi implements MementoExplorerExport { +export class DockerExtensionApi implements MementoExplorerExport, DockerExtensionRegistryApi { readonly #extensionMementos: ExtensionMementos | undefined; public constructor(ctx: vscode.ExtensionContext) { @@ -18,6 +20,10 @@ export class DockerExtensionApi implements MementoExplorerExport { } } + public registerRegistryDataProvider(id: string, registryDataProvider: RegistryDataProvider): vscode.Disposable { + return ext.registriesTree.registerProvider(registryDataProvider); + } + public get memento(): ExtensionMementos | undefined { return this.#extensionMementos; } diff --git a/src/commands/images/pushImage.ts b/src/commands/images/pushImage.ts index 5ac02c613e..421e25c1e6 100644 --- a/src/commands/images/pushImage.ts +++ b/src/commands/images/pushImage.ts @@ -4,12 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext, NoResourceFoundError } from '@microsoft/vscode-azext-utils'; +import { parseDockerLikeImageName } from '@microsoft/vscode-container-client'; +import { CommonRegistry } from '@microsoft/vscode-docker-registries'; import * as vscode from 'vscode'; import { ext } from '../../extensionVariables'; import { TaskCommandRunnerFactory } from '../../runtimes/runners/TaskCommandRunnerFactory'; import { ImageTreeItem } from '../../tree/images/ImageTreeItem'; -import { registryExpectedContextValues } from '../../tree/registries/registryContextValues'; -import { RegistryTreeItemBase } from '../../tree/registries/RegistryTreeItemBase'; +import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { registryExperience } from '../../utils/registryExperience'; import { addImageTaggingTelemetry, tagImage } from './tagImage'; export async function pushImage(context: IActionContext, node: ImageTreeItem | undefined): Promise { @@ -21,7 +23,7 @@ export async function pushImage(context: IActionContext, node: ImageTreeItem | u }); } - let connectedRegistry: RegistryTreeItemBase | undefined; + let connectedRegistry: UnifiedRegistryItem | undefined; if (!node.fullTag.includes('/')) { // The registry to push to is indeterminate--could be Docker Hub, or could need tagging. @@ -30,7 +32,7 @@ export async function pushImage(context: IActionContext, node: ImageTreeItem | u // If the prompt setting is true, we'll ask; if not we'll assume Docker Hub. if (prompt) { try { - connectedRegistry = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.all.registry, context); + connectedRegistry = await registryExperience(context, { contextValueFilter: { include: [/commonregistry/i] } }); } catch (error) { if (error instanceof NoResourceFoundError) { // Do nothing, move on without a selected registry @@ -41,25 +43,30 @@ export async function pushImage(context: IActionContext, node: ImageTreeItem | u } } else { // Try to find a connected Docker Hub registry (primarily for login credentials) - connectedRegistry = await tryGetDockerHubRegistry(context); + connectedRegistry = await registryExperience( + context, + { + registryFilter: { include: [ext.dockerHubRegistryDataProvider.label] }, + contextValueFilter: { include: /commonregistry/i }, + skipIfOne: true + } + ); } } else { // The registry to push to is determinate. If there's a connected registry in the tree view, we'll try to find it, to perform login ahead of time. // Registry path is everything up to the last slash. - const baseImagePath = node.fullTag.substring(0, node.fullTag.lastIndexOf('/')); - const progressOptions: vscode.ProgressOptions = { location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Fetching login credentials...'), }; - connectedRegistry = await vscode.window.withProgress(progressOptions, async () => await tryGetConnectedRegistryForPath(context, baseImagePath)); + connectedRegistry = await vscode.window.withProgress(progressOptions, async () => await tryGetConnectedRegistryForPath(context, node.parent.label)); } // Give the user a chance to modify the tag however they want const finalTag = await tagImage(context, node, connectedRegistry); - - if (connectedRegistry && finalTag.startsWith(connectedRegistry.baseImagePath)) { + const baseImagePath = connectedRegistry.wrappedItem.baseUrl.authority; + if (connectedRegistry && finalTag.startsWith(baseImagePath)) { // If a registry was found/chosen and is still the same as the final tag's registry, try logging in await vscode.commands.executeCommand('vscode-docker.registries.logInToDockerCli', connectedRegistry); } @@ -78,12 +85,15 @@ export async function pushImage(context: IActionContext, node: ImageTreeItem | u ); } -async function tryGetConnectedRegistryForPath(context: IActionContext, baseImagePath: string): Promise { - const allRegistries = await ext.registriesRoot.getAllConnectedRegistries(context); - return allRegistries.find(r => r.baseImagePath === baseImagePath); -} +async function tryGetConnectedRegistryForPath(context: IActionContext, baseImagePath: string): Promise | undefined> { + const baseImageNameInfo = parseDockerLikeImageName(baseImagePath); + const allRegistries = await ext.registriesTree.getConnectedRegistries(baseImageNameInfo.registry); + + let matchedRegistry = allRegistries.find((registry) => registry.wrappedItem.baseUrl.authority === baseImageNameInfo.registry); + + if (!matchedRegistry) { + matchedRegistry = await registryExperience(context, { contextValueFilter: { include: [/commonregistry/i] } }); + } -async function tryGetDockerHubRegistry(context: IActionContext): Promise { - const allRegistries = await ext.registriesRoot.getAllConnectedRegistries(context); - return allRegistries.find(r => r.contextValue.match(registryExpectedContextValues.dockerHub.registry)); + return matchedRegistry; } diff --git a/src/commands/images/tagImage.ts b/src/commands/images/tagImage.ts index 8f00530ce8..ca4bb7c34b 100644 --- a/src/commands/images/tagImage.ts +++ b/src/commands/images/tagImage.ts @@ -4,12 +4,14 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext, TelemetryProperties } from '@microsoft/vscode-azext-utils'; +import { CommonRegistry, isRegistry } from '@microsoft/vscode-docker-registries'; import * as vscode from 'vscode'; import { ext } from '../../extensionVariables'; import { ImageTreeItem } from '../../tree/images/ImageTreeItem'; -import { RegistryTreeItemBase } from '../../tree/registries/RegistryTreeItemBase'; +import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { getBaseImagePathFromRegistry } from '../../tree/registries/registryTreeUtils'; -export async function tagImage(context: IActionContext, node?: ImageTreeItem, registry?: RegistryTreeItemBase): Promise { +export async function tagImage(context: IActionContext, node?: ImageTreeItem, registry?: UnifiedRegistryItem): Promise { if (!node) { await ext.imagesTree.refresh(context); node = await ext.imagesTree.showTreeItemPicker(ImageTreeItem.contextValue, { @@ -19,7 +21,8 @@ export async function tagImage(context: IActionContext, node?: ImageTreeItem, re } addImageTaggingTelemetry(context, node.fullTag, '.before'); - const newTaggedName: string = await getTagFromUserInput(context, node.fullTag, registry?.baseImagePath); + const baseImagePath = isRegistry(registry.wrappedItem) ? getBaseImagePathFromRegistry(registry.wrappedItem) : undefined; + const newTaggedName: string = await getTagFromUserInput(context, node.fullTag, baseImagePath); addImageTaggingTelemetry(context, newTaggedName, '.after'); await ext.runWithDefaults(client => diff --git a/src/commands/registerCommands.ts b/src/commands/registerCommands.ts index 00916b14cd..e317a03568 100644 --- a/src/commands/registerCommands.ts +++ b/src/commands/registerCommands.ts @@ -58,17 +58,16 @@ 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"; -import { runFileAsAzureTask } from "./registries/azure/tasks/runFileAsAzureTask"; -import { viewAzureTaskLogs } from "./registries/azure/tasks/viewAzureTaskLogs"; import { untagAzureImage } from "./registries/azure/untagAzureImage"; import { viewAzureProperties } from "./registries/azure/viewAzureProperties"; import { connectRegistry } from "./registries/connectRegistry"; -import { copyRemoteFullTag } from './registries/copyRemoteFullTag'; +import { copyRemoteFullTag } from "./registries/copyRemoteFullTag"; import { copyRemoteImageDigest } from "./registries/copyRemoteImageDigest"; import { deleteRemoteImage } from "./registries/deleteRemoteImage"; import { disconnectRegistry } from "./registries/disconnectRegistry"; import { openDockerHubInBrowser } from "./registries/dockerHub/openDockerHubInBrowser"; +import { addTrackedGenericV2Registry } from "./registries/genericV2/addTrackedGenericV2Registry"; +import { removeTrackedGenericV2Registry } from "./registries/genericV2/removeTrackedGenericV2Registry"; import { logInToDockerCli } from "./registries/logInToDockerCli"; import { logOutOfDockerCli } from "./registries/logOutOfDockerCli"; import { pullImageFromRepository, pullRepository } from "./registries/pullImages"; @@ -180,6 +179,9 @@ export function registerCommands(): void { registerWorkspaceCommand('vscode-docker.registries.pullRepository', pullRepository); registerCommand('vscode-docker.registries.reconnectRegistry', reconnectRegistry); + registerCommand('vscode-docker.registries.genericV2.removeTrackedRegistry', removeTrackedGenericV2Registry); + registerCommand('vscode-docker.registries.genericV2.addTrackedRegistry', addTrackedGenericV2Registry); + registerCommand('vscode-docker.registries.dockerHub.openInBrowser', openDockerHubInBrowser); registerWorkspaceCommand('vscode-docker.registries.azure.buildImage', buildImageInAzure); @@ -187,12 +189,9 @@ export function registerCommands(): void { registerCommand('vscode-docker.registries.azure.deleteRegistry', deleteAzureRegistry); registerCommand('vscode-docker.registries.azure.deleteRepository', deleteAzureRepository); registerCommand('vscode-docker.registries.azure.openInPortal', openInAzurePortal); - registerCommand('vscode-docker.registries.azure.runTask', runAzureTask); - registerWorkspaceCommand('vscode-docker.registries.azure.runFileAsTask', runFileAsAzureTask); registerCommand('vscode-docker.registries.azure.selectSubscriptions', () => commands.executeCommand("azure-account.selectSubscriptions")); registerCommand('vscode-docker.registries.azure.untagImage', untagAzureImage); registerCommand('vscode-docker.registries.azure.viewProperties', viewAzureProperties); - registerCommand('vscode-docker.registries.azure.viewTaskLogs', viewAzureTaskLogs); registerCommand('vscode-docker.volumes.configureExplorer', configureVolumesExplorer); registerCommand('vscode-docker.volumes.inspect', inspectVolume); @@ -208,4 +207,10 @@ export function registerCommands(): void { registerCommand('vscode-docker.openDockerDownloadPage', openDockerDownloadPage); registerCommand('vscode-docker.help', help); registerCommand('vscode-docker.help.openWalkthrough', () => commands.executeCommand('workbench.action.openWalkthrough', 'ms-azuretools.vscode-docker#dockerStart')); + + registerCommand('vscode-docker.activateRegistryProviders', (context: IActionContext) => { + // Do nothing, but registry provider extensions can use this command as an activation event + context.telemetry.suppressAll = true; + context.errorHandling.suppressDisplay = true; + }); } diff --git a/src/commands/registries/azure/DockerAssignAcrPullRoleStep.ts b/src/commands/registries/azure/DockerAssignAcrPullRoleStep.ts index 1b310433f0..56b4ce2379 100644 --- a/src/commands/registries/azure/DockerAssignAcrPullRoleStep.ts +++ b/src/commands/registries/azure/DockerAssignAcrPullRoleStep.ts @@ -5,17 +5,19 @@ 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 { l10n, Progress } from "vscode"; +import { Progress, l10n } from "vscode"; import { ext } from "../../../extensionVariables"; -import { AzureRegistryTreeItem } from '../../../tree/registries/azure/AzureRegistryTreeItem'; -import { RemoteTagTreeItem } from '../../../tree/registries/RemoteTagTreeItem'; +import { AzureRegistry, isAzureTag } 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: RemoteTagTreeItem) { + public constructor(private readonly tagTreeItem: UnifiedRegistryItem) { super(); } @@ -33,14 +35,14 @@ export class DockerAssignAcrPullRoleStep extends AzureWizardExecuteStep = this.tagTreeItem.parent.parent as unknown as UnifiedRegistryItem; // 1. Get the registry resource. We will need the ID. - const registry = await crmClient.registries.get(registryTreeItem.resourceGroup, registryTreeItem.registryName); + 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.baseUrl) + l10n.t('Unable to get details from Container Registry {0}', registryTreeItem.wrappedItem.label) ); } @@ -78,12 +80,12 @@ export class DockerAssignAcrPullRoleStep extends AzureWizardExecuteStep { public priority: number = 140; - public constructor(private readonly node: RemoteTagTreeItem) { + public constructor(private readonly tagItem: UnifiedRegistryItem) { super(); } @@ -49,7 +46,7 @@ export class DockerSiteCreateStep extends AzureWizardExecuteStep { - const registryTI: RegistryTreeItemBase = this.node.parent.parent; + const registryTI: UnifiedRegistryItem = this.tagItem.parent.parent as unknown as UnifiedRegistryItem; let username: string | undefined; let password: string | undefined; @@ -69,7 +66,7 @@ export class DockerSiteCreateStep extends AzureWizardExecuteStep App Service, NOT Arc App Service. Use managed service identity. - if (registryTI instanceof AzureRegistryTreeItem && !context.customLocation) { + if (isAzureRegistry(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 @@ -80,36 +77,29 @@ export class DockerSiteCreateStep extends AzureWizardExecuteStep 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); + else if (isAzureRegistry(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.baseUrl; + registryUrl = registryTI.wrappedItem.baseUrl.toString(); } - // 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)); + // Other registries -> App Service *OR* Arc App Service + else { + if (!registryTI.provider.getLoginInformation) { + throw new Error(l10n.t('This registry does not support deploying to Azure App Service')); } + const loginInformation = await registryTI.provider.getLoginInformation(registryTI.wrappedItem); - registryUrl = registryTI.baseUrl; - } else { - throw new RangeError(l10n.t('Unrecognized node type "{0}"', registryTI.constructor.name)); + registryUrl = (registryTI.wrappedItem as CommonRegistry).baseUrl.toString(); + username = loginInformation.username; + password = loginInformation.secret; } + if (username && password) { appSettings.push({ name: "DOCKER_REGISTRY_SERVER_USERNAME", value: username }); appSettings.push({ name: "DOCKER_REGISTRY_SERVER_PASSWORD", value: password }); @@ -123,7 +113,7 @@ export class DockerSiteCreateStep extends AzureWizardExecuteStep { public priority: number = 142; // execute after DockerAssignAcrPullRoleStep - private _treeItem: RemoteTagTreeItem; - public constructor(treeItem: RemoteTagTreeItem) { + + public constructor(private readonly tagItem: UnifiedRegistryItem) { super(); - this._treeItem = treeItem; } public async execute(context: IAppServiceWizardContext, progress: vscode.Progress<{ @@ -34,17 +33,20 @@ export class DockerWebhookCreateStep extends AzureWizardExecuteStep//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`; + const dockerhubUri: string = `https://cloud.docker.com/repository/docker/${registryName}/${repoName}/webHooks`; // 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 @@ -64,10 +66,10 @@ export class DockerWebhookCreateStep extends AzureWizardExecuteStep { + private async createWebhookForApp(context: IAppServiceWizardContext, site: Site, appUri: string): Promise { const maxLength: number = 50; const numRandomChars: number = 6; @@ -80,17 +82,18 @@ export class DockerWebhookCreateStep extends AzureWizardExecuteStepnode.parent).parent; + 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.registryLocation, + location: registryTreeItem.wrappedItem.registryProperties.location, serviceUri: appUri, - scope: `${node.parent.repoName}:${node.tag}`, + scope: `${this.tagItem.parent.wrappedItem.label}:${this.tagItem.wrappedItem.label}`, actions: ["push"], status: 'enabled' }; - return await crmClient.webhooks.beginCreateAndWait(registryTreeItem.resourceGroup, registryTreeItem.registryName, webhookName, webhookCreateParameters); + const resourceGroup = getResourceGroupFromAzureRegistryItem(registryTreeItem.wrappedItem); + return await crmClient.webhooks.beginCreateAndWait(resourceGroup, registryTreeItem.wrappedItem.label, webhookName, webhookCreateParameters); } } diff --git a/src/commands/registries/azure/createAzureRegistry.ts b/src/commands/registries/azure/createAzureRegistry.ts index e68cf2c0a4..137be90bb3 100644 --- a/src/commands/registries/azure/createAzureRegistry.ts +++ b/src/commands/registries/azure/createAzureRegistry.ts @@ -3,17 +3,61 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionContext } from '@microsoft/vscode-azext-utils'; +import { AzureWizard, IActionContext, createSubscriptionContext, nonNullProp } from '@microsoft/vscode-azext-utils'; +import { l10n, window } from 'vscode'; import { ext } from '../../../extensionVariables'; -import type { SubscriptionTreeItem } from '../../../tree/registries/azure/SubscriptionTreeItem'; // These are only dev-time imports so don't need to be lazy -import { getAzSubTreeItem } from '../../../utils/lazyPackages'; +import { AzureSubscriptionRegistryItem } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { AzureRegistryCreateStep } from '../../../tree/registries/Azure/createWizard/AzureRegistryCreateStep'; +import { AzureRegistryNameStep } from '../../../tree/registries/Azure/createWizard/AzureRegistryNameStep'; +import { AzureRegistrySkuStep } from '../../../tree/registries/Azure/createWizard/AzureRegistrySkuStep'; +import { IAzureRegistryWizardContext } from '../../../tree/registries/Azure/createWizard/IAzureRegistryWizardContext'; +import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { getAzExtAzureUtils } from '../../../utils/lazyPackages'; +import { registryExperience } from '../../../utils/registryExperience'; -export async function createAzureRegistry(context: IActionContext, node?: SubscriptionTreeItem): Promise { - const azSubTreeItem = await getAzSubTreeItem(); +export async function createAzureRegistry(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(azSubTreeItem.SubscriptionTreeItem.contextValue, context); + node = await registryExperience(context, + { + contextValueFilter: { include: /azuresubscription/i }, + registryFilter: { include: [ext.azureRegistryDataProvider.label] } + } + ); } - await node.createChild(context); + const registryItem = node.wrappedItem; + + const subscriptionContext = createSubscriptionContext(registryItem.subscription); + const wizardContext: IAzureRegistryWizardContext = { + ...context, + ...subscriptionContext, + azureSubscription: registryItem.subscription, + }; + const azExtAzureUtils = await getAzExtAzureUtils(); + + const promptSteps = [ + new AzureRegistryNameStep(), + new AzureRegistrySkuStep(), + new azExtAzureUtils.ResourceGroupListStep(), + ]; + azExtAzureUtils.LocationListStep.addStep(wizardContext, promptSteps); + + const wizard = new AzureWizard( + wizardContext, + { + promptSteps, + executeSteps: [ + new AzureRegistryCreateStep() + ], + title: l10n.t('Create new Azure Container Registry') + } + ); + + await wizard.prompt(); + const newRegistryName: string = nonNullProp(wizardContext, 'newRegistryName'); + await wizard.execute(); + + void window.showInformationMessage(`Successfully created registry "${newRegistryName}".`); + void ext.registriesTree.refresh(); } diff --git a/src/commands/registries/azure/deleteAzureRegistry.ts b/src/commands/registries/azure/deleteAzureRegistry.ts index 862a3582d6..949256b195 100644 --- a/src/commands/registries/azure/deleteAzureRegistry.ts +++ b/src/commands/registries/azure/deleteAzureRegistry.ts @@ -4,26 +4,35 @@ *--------------------------------------------------------------------------------------------*/ import { DialogResponses, IActionContext } from '@microsoft/vscode-azext-utils'; -import { l10n, ProgressLocation, window } from 'vscode'; +import { ProgressLocation, l10n, window } from 'vscode'; import { ext } from '../../../extensionVariables'; -import type { AzureRegistryTreeItem } from '../../../tree/registries/azure/AzureRegistryTreeItem'; -import { registryExpectedContextValues } from '../../../tree/registries/registryContextValues'; +import { AzureRegistry, AzureRegistryDataProvider } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { registryExperience } from '../../../utils/registryExperience'; -export async function deleteAzureRegistry(context: IActionContext, node?: AzureRegistryTreeItem): Promise { +export async function deleteAzureRegistry(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.azure.registry, { ...context, suppressCreatePick: true }); + node = await registryExperience(context, { + contextValueFilter: { include: /commonregistry/i }, + registryFilter: { include: [ext.azureRegistryDataProvider.label] } + }); } - const confirmDelete: string = l10n.t('Are you sure you want to delete registry "{0}" and its associated images?', node.registryName); + const registryName = node.wrappedItem.label; + + const confirmDelete: string = l10n.t('Are you sure you want to delete registry "{0}" and its associated images?', registryName); // no need to check result - cancel will throw a UserCancelledError await context.ui.showWarningMessage(confirmDelete, { modal: true }, DialogResponses.deleteResponse); - const deleting = l10n.t('Deleting registry "{0}"...', node.registryName); + const deleting = l10n.t('Deleting registry "{0}"...', registryName); await window.withProgress({ location: ProgressLocation.Notification, title: deleting }, async () => { - await node.deleteTreeItem(context); + const azureRegistryDataProvider = node.provider as unknown as AzureRegistryDataProvider; + await azureRegistryDataProvider.deleteRegistry(node.wrappedItem); }); - const message = l10n.t('Successfully deleted registry "{0}".', node.registryName); + void ext.registriesTree.refresh(); + + const message = l10n.t('Successfully deleted registry "{0}".', registryName); // don't wait /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ window.showInformationMessage(message); diff --git a/src/commands/registries/azure/deleteAzureRepository.ts b/src/commands/registries/azure/deleteAzureRepository.ts index c3be2f59f5..f0fc6bbbef 100644 --- a/src/commands/registries/azure/deleteAzureRepository.ts +++ b/src/commands/registries/azure/deleteAzureRepository.ts @@ -4,27 +4,32 @@ *--------------------------------------------------------------------------------------------*/ import { DialogResponses, IActionContext } from '@microsoft/vscode-azext-utils'; -import { l10n, ProgressLocation, window } from 'vscode'; +import { ProgressLocation, l10n, window } from 'vscode'; import { ext } from '../../../extensionVariables'; -import type { AzureRepositoryTreeItem } from '../../../tree/registries/azure/AzureRepositoryTreeItem'; -import { registryExpectedContextValues } from '../../../tree/registries/registryContextValues'; +import { AzureRegistryDataProvider, AzureRepository } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { registryExperience } from '../../../utils/registryExperience'; -export async function deleteAzureRepository(context: IActionContext, node?: AzureRepositoryTreeItem): Promise { +export async function deleteAzureRepository(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.azure.repository, { ...context, suppressCreatePick: true }); + node = await registryExperience(context, { + contextValueFilter: { include: /commonrepository/i }, + registryFilter: { include: [ext.azureRegistryDataProvider.label] } + }); } - const confirmDelete = l10n.t('Are you sure you want to delete repository "{0}" and its associated images?', node.repoName); + const confirmDelete = l10n.t('Are you sure you want to delete repository "{0}" and its associated images?', node.wrappedItem.label); // no need to check result - cancel will throw a UserCancelledError await context.ui.showWarningMessage(confirmDelete, { modal: true }, DialogResponses.deleteResponse); - const deleting = l10n.t('Deleting repository "{0}"...', node.repoName); + const deleting = l10n.t('Deleting repository "{0}"...', node.wrappedItem.label); await window.withProgress({ location: ProgressLocation.Notification, title: deleting }, async () => { - await node.deleteTreeItem(context); + const azureDataProvider = node.provider as unknown as AzureRegistryDataProvider; + await azureDataProvider.deleteRepository(node.wrappedItem); }); - const deleteSucceeded = l10n.t('Successfully deleted repository "{0}".', node.repoName); - // don't wait - /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ - window.showInformationMessage(deleteSucceeded); + void ext.registriesTree.refresh(); + + const deleteSucceeded = l10n.t('Successfully deleted repository "{0}".', node.wrappedItem.label); + void window.showInformationMessage(deleteSucceeded); } diff --git a/src/commands/registries/azure/deployImageToAca.ts b/src/commands/registries/azure/deployImageToAca.ts index f31a57c7cd..89b447eff9 100644 --- a/src/commands/registries/azure/deployImageToAca.ts +++ b/src/commands/registries/azure/deployImageToAca.ts @@ -5,15 +5,14 @@ import { IActionContext, nonNullProp, UserCancelledError } from '@microsoft/vscode-azext-utils'; import { parseDockerLikeImageName } from '@microsoft/vscode-container-client'; +import { CommonRegistry, CommonTag, isDockerHubRegistry, LoginInformation } from '@microsoft/vscode-docker-registries'; import * as semver from 'semver'; import * as vscode from 'vscode'; -import { ext } from '../../../extensionVariables'; -import { AzureRegistryTreeItem } from '../../../tree/registries/azure/AzureRegistryTreeItem'; -import { DockerHubNamespaceTreeItem } from '../../../tree/registries/dockerHub/DockerHubNamespaceTreeItem'; -import { registryExpectedContextValues } from '../../../tree/registries/registryContextValues'; -import { RegistryTreeItemBase } from '../../../tree/registries/RegistryTreeItemBase'; -import { RemoteTagTreeItem } from '../../../tree/registries/RemoteTagTreeItem'; +import { isAzureRegistry } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { getFullImageNameFromRegistryTagItem } from '../../../tree/registries/registryTreeUtils'; +import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; import { installExtension } from '../../../utils/installExtension'; +import { registryExperience } from '../../../utils/registryExperience'; import { addImageTaggingTelemetry } from '../../images/tagImage'; const acaExtensionId = 'ms-azuretools.vscode-azurecontainerapps'; @@ -27,7 +26,7 @@ interface DeployImageToAcaOptionsContract { secret?: string; } -export async function deployImageToAca(context: IActionContext, node?: RemoteTagTreeItem): Promise { +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 @@ -36,34 +35,35 @@ export async function deployImageToAca(context: IActionContext, node?: RemoteTag } if (!node) { - node = await ext.registriesTree.showTreeItemPicker([registryExpectedContextValues.dockerHub.tag, registryExpectedContextValues.dockerV2.tag], context); + node = await registryExperience(context, { contextValueFilter: { include: /commontag/i } }); } const commandOptions: Partial = { - image: node.fullTag, + image: getFullImageNameFromRegistryTagItem(node.wrappedItem), }; addImageTaggingTelemetry(context, commandOptions.image, ''); - const registry: RegistryTreeItemBase = node.parent.parent; - if (registry instanceof AzureRegistryTreeItem) { + const registry: UnifiedRegistryItem = node.parent.parent as unknown as UnifiedRegistryItem; + + if (isAzureRegistry(registry.wrappedItem)) { // No additional work to do; ACA can handle this on its own } else { - const { auth } = await registry.getDockerCliCredentials() as { auth?: { username?: string, password?: string } }; + const logInInfo: LoginInformation = await registry.provider.getLoginInformation(registry.wrappedItem); - if (!auth?.username || !auth?.password) { - throw new Error(vscode.l10n.t('No credentials found for registry "{0}".', registry.label)); + if (!logInInfo?.username || !logInInfo?.secret) { + throw new Error(vscode.l10n.t('No credentials found for registry "{0}".', registry.wrappedItem.label)); } - if (registry instanceof DockerHubNamespaceTreeItem) { + if (isDockerHubRegistry(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 = auth.username; - commandOptions.secret = auth.password; + commandOptions.username = logInInfo.username; + commandOptions.secret = logInInfo.secret; } commandOptions.registryName = nonNullProp(parseDockerLikeImageName(commandOptions.image), 'registry'); diff --git a/src/commands/registries/azure/deployImageToAzure.ts b/src/commands/registries/azure/deployImageToAzure.ts index bbc3cee850..7bba3abb90 100644 --- a/src/commands/registries/azure/deployImageToAzure.ts +++ b/src/commands/registries/azure/deployImageToAzure.ts @@ -5,45 +5,43 @@ 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 { AzureWizard, AzureWizardExecuteStep, AzureWizardPromptStep, IActionContext, 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 { 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 { 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 async function deployImageToAzure(context: IActionContext, node?: RemoteTagTreeItem): Promise { +export async function deployImageToAzure(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker([registryExpectedContextValues.dockerHub.tag, registryExpectedContextValues.dockerV2.tag], context); + node = await registryExperience(context, { contextValueFilter: { include: 'commontag' } }); } const azExtAzureUtils = await getAzExtAzureUtils(); const vscAzureAppService = await getAzExtAppService(); - const azActTreeItem = await getAzActTreeItem(); + const promptSteps: AzureWizardPromptStep[] = []; + const subscriptionItem = await registryExperience(context, { + registryFilter: { include: [ext.azureRegistryDataProvider.label] }, + contextValueFilter: { include: /azuresubscription/i }, + }); + const subscriptionContext = createSubscriptionContext(subscriptionItem.wrappedItem.subscription); const wizardContext: IActionContext & Partial = { ...context, + ...subscriptionContext, 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); - } promptSteps.push(new vscAzureAppService.SiteNameStep()); promptSteps.push(new azExtAzureUtils.ResourceGroupListStep()); diff --git a/src/commands/registries/azure/openInAzurePortal.ts b/src/commands/registries/azure/openInAzurePortal.ts index bec849c454..f72b11b2d2 100644 --- a/src/commands/registries/azure/openInAzurePortal.ts +++ b/src/commands/registries/azure/openInAzurePortal.ts @@ -3,27 +3,32 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionContext } from '@microsoft/vscode-azext-utils'; +import { IActionContext, createSubscriptionContext } from '@microsoft/vscode-azext-utils'; import { ext } from '../../../extensionVariables'; -import { AzureRegistryTreeItem } from '../../../tree/registries/azure/AzureRegistryTreeItem'; -import { AzureRepositoryTreeItem } from '../../../tree/registries/azure/AzureRepositoryTreeItem'; -import type { SubscriptionTreeItem } from '../../../tree/registries/azure/SubscriptionTreeItem'; // These are only dev-time imports so don't need to be lazy -import { registryExpectedContextValues } from '../../../tree/registries/registryContextValues'; -import { getAzExtAzureUtils, getAzSubTreeItem } from '../../../utils/lazyPackages'; +import { AzureRegistry, AzureRepository, AzureSubscriptionRegistryItem, isAzureRegistry, isAzureSubscriptionRegistryItem } from '../../../tree/registries/Azure/AzureRegistryDataProvider'; +import { UnifiedRegistryItem } from '../../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { getAzExtAzureUtils } from '../../../utils/lazyPackages'; +import { registryExperience } from '../../../utils/registryExperience'; -export async function openInAzurePortal(context: IActionContext, node?: SubscriptionTreeItem | AzureRegistryTreeItem | AzureRepositoryTreeItem): Promise { +export async function openInAzurePortal(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.azure.registry, context); + node = await registryExperience(context, { + registryFilter: { include: [ext.azureRegistryDataProvider.label] }, + contextValueFilter: { include: [/commonregistry/i] }, + }); } - const azSubTreeItem = await getAzSubTreeItem(); + const azureRegistryItem = node.wrappedItem; const azExtAzureUtils = await getAzExtAzureUtils(); - - if (node instanceof azSubTreeItem.SubscriptionTreeItem) { - await azExtAzureUtils.openInPortal(node.subscription, node.subscription.subscriptionId); - } else if (node instanceof AzureRegistryTreeItem) { - await azExtAzureUtils.openInPortal(node.parent.subscription, node.registryId); + let subscriptionContext = undefined; + if (isAzureSubscriptionRegistryItem(azureRegistryItem)) { + subscriptionContext = createSubscriptionContext(azureRegistryItem.subscription); + await azExtAzureUtils.openInPortal(subscriptionContext, `/subscriptions/${subscriptionContext.subscriptionId}`); + } else if (isAzureRegistry(azureRegistryItem)) { + subscriptionContext = createSubscriptionContext(azureRegistryItem.parent.subscription); + await azExtAzureUtils.openInPortal(subscriptionContext, azureRegistryItem.id); } else { - await azExtAzureUtils.openInPortal(node.parent.parent.subscription, `${node.parent.registryId}/repository`); + subscriptionContext = createSubscriptionContext(azureRegistryItem.parent.parent.subscription); + await azExtAzureUtils.openInPortal(subscriptionContext, `${azureRegistryItem.parent.id}/repository`); } } diff --git a/src/commands/registries/azure/tasks/buildImageInAzure.ts b/src/commands/registries/azure/tasks/buildImageInAzure.ts index 346606bde5..7d1cca1471 100644 --- a/src/commands/registries/azure/tasks/buildImageInAzure.ts +++ b/src/commands/registries/azure/tasks/buildImageInAzure.ts @@ -3,12 +3,12 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; -import { IActionContext, UserCancelledError } from '@microsoft/vscode-azext-utils'; import type { Run } from '@azure/arm-containerregistry'; -import { scheduleRunRequest, RootStrategy } from './scheduleRunRequest'; -import { delay } from '../../../../utils/promiseUtils'; +import { IActionContext, UserCancelledError } from '@microsoft/vscode-azext-utils'; +import * as vscode from 'vscode'; import { getArmContainerRegistry } from '../../../../utils/lazyPackages'; +import { delay } from '../../../../utils/promiseUtils'; +import { RootStrategy, scheduleRunRequest } from './scheduleRunRequest'; const WAIT_MS = 5000; @@ -18,7 +18,7 @@ export async function buildImageInAzure(context: IActionContext, uri?: vscode.Ur } const getRun = await scheduleRunRequest(context, "DockerBuildRequest", uri, rootStrategy); - + let run = await getRun(); const { KnownRunStatus } = await getArmContainerRegistry(); while ( diff --git a/src/commands/registries/azure/tasks/runAzureTask.ts b/src/commands/registries/azure/tasks/runAzureTask.ts deleted file mode 100644 index 0be5e31898..0000000000 --- a/src/commands/registries/azure/tasks/runAzureTask.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import type { TaskRunRequest } from "@azure/arm-containerregistry"; // These are only dev-time imports so don't need to be lazy -import { IActionContext } from "@microsoft/vscode-azext-utils"; -import { l10n, window } from "vscode"; -import { ext } from "../../../../extensionVariables"; -import { AzureTaskTreeItem } from "../../../../tree/registries/azure/AzureTaskTreeItem"; - -export async function runAzureTask(context: IActionContext, node?: AzureTaskTreeItem): Promise { - if (!node) { - node = await ext.registriesTree.showTreeItemPicker(AzureTaskTreeItem.contextValue, context); - } - - const registryTI = node.parent.parent; - const runRequest: TaskRunRequest = { type: 'TaskRunRequest', taskId: node.id }; - const run = await (await registryTI.getClient(context)).registries.beginScheduleRunAndWait(registryTI.resourceGroup, registryTI.registryName, runRequest); - await node.parent.refresh(context); - // don't wait - /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ - window.showInformationMessage(l10n.t('Successfully scheduled run "{0}" for task "{1}".', run.runId, node.taskName)); -} diff --git a/src/commands/registries/azure/tasks/runFileAsAzureTask.ts b/src/commands/registries/azure/tasks/runFileAsAzureTask.ts deleted file mode 100644 index 92e5ac28b9..0000000000 --- a/src/commands/registries/azure/tasks/runFileAsAzureTask.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as vscode from 'vscode'; -import { IActionContext, UserCancelledError } from '@microsoft/vscode-azext-utils'; -import { scheduleRunRequest } from './scheduleRunRequest'; - -export async function runFileAsAzureTask(context: IActionContext, uri?: vscode.Uri): Promise { - if (!vscode.workspace.isTrusted) { - throw new UserCancelledError('enforceTrust'); - } - - await scheduleRunRequest(context, "FileTaskRunRequest", uri); -} diff --git a/src/commands/registries/azure/tasks/scheduleRunRequest.ts b/src/commands/registries/azure/tasks/scheduleRunRequest.ts index 66f0dfc5f9..02e680aff8 100644 --- a/src/commands/registries/azure/tasks/scheduleRunRequest.ts +++ b/src/commands/registries/azure/tasks/scheduleRunRequest.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { ContainerRegistryManagementClient, DockerBuildRequest as AcrDockerBuildRequest, FileTaskRunRequest as AcrFileTaskRunRequest, OS as AcrOS, Run as AcrRun } from "@azure/arm-containerregistry"; // These are only dev-time imports so don't need to be lazy +import type { DockerBuildRequest as AcrDockerBuildRequest, FileTaskRunRequest as AcrFileTaskRunRequest, OS as AcrOS, Run as AcrRun, ContainerRegistryManagementClient } from "@azure/arm-containerregistry"; // These are only dev-time imports so don't need to be lazy import { IActionContext, IAzureQuickPickItem, nonNullProp } from '@microsoft/vscode-azext-utils'; import * as fse from 'fs-extra'; import * as os from 'os'; @@ -12,12 +12,14 @@ import * as readline from 'readline'; import * as tar from 'tar'; import * as vscode from 'vscode'; import { ext } from '../../../../extensionVariables'; -import { AzureRegistryTreeItem } from '../../../../tree/registries/azure/AzureRegistryTreeItem'; -import { registryExpectedContextValues } from "../../../../tree/registries/registryContextValues"; +import { AzureRegistry, AzureRegistryItem } from "../../../../tree/registries/Azure/AzureRegistryDataProvider"; +import { UnifiedRegistryItem } from "../../../../tree/registries/UnifiedRegistryTreeDataProvider"; +import { createAzureContainerRegistryClient, getResourceGroupFromId } from "../../../../utils/azureUtils"; import { getStorageBlob } from '../../../../utils/lazyPackages'; import { delay } from '../../../../utils/promiseUtils'; import { Item, quickPickDockerFileItem, quickPickYamlFileItem } from '../../../../utils/quickPickFile'; import { quickPickWorkspaceFolder } from '../../../../utils/quickPickWorkspaceFolder'; +import { registryExperience } from "../../../../utils/registryExperience"; import { addImageTaggingTelemetry, getTagFromUserInput } from '../../../images/tagImage'; const idPrecision = 6; @@ -45,7 +47,12 @@ export async function scheduleRunRequest(context: IActionContext, requestType: ' throw new Error(vscode.l10n.t('Run Request Type Currently not supported.')); } - const node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.azure.registry, context); + const node: UnifiedRegistryItem = await registryExperience(context, { + registryFilter: { include: [ext.azureRegistryDataProvider.label] }, + contextValueFilter: { include: /commonregistry/i }, + }); + const registryItem: AzureRegistryItem = node.wrappedItem; + const resourceGroup = getResourceGroupFromId(registryItem.id); const osPick = ['Linux', 'Windows'].map(item => >{ label: item, data: item }); const osType: AcrOS = (await context.ui.showQuickPick(osPick, { placeHolder: vscode.l10n.t('Select image base OS') })).data; @@ -63,7 +70,8 @@ export async function scheduleRunRequest(context: IActionContext, requestType: ' rootUri = vscode.Uri.file(path.dirname(fileItem.absoluteFilePath)); } - const uploadedSourceLocation: string = await uploadSourceCode(await node.getClient(context), node.registryName, node.resourceGroup, rootUri, tarFilePath); + const azureRegistryClient = await createAzureContainerRegistryClient(registryItem.subscription); + const uploadedSourceLocation: string = await uploadSourceCode(azureRegistryClient, registryItem.label, resourceGroup, rootUri, tarFilePath); ext.outputChannel.info(vscode.l10n.t('Uploaded source code from {0}', tarFilePath)); let runRequest: AcrDockerBuildRequest | AcrFileTaskRunRequest; @@ -88,14 +96,13 @@ export async function scheduleRunRequest(context: IActionContext, requestType: ' // Schedule the run and Clean up. ext.outputChannel.info(vscode.l10n.t('Set up run request')); - const client = await node.getClient(context); - const run = await client.registries.beginScheduleRunAndWait(node.resourceGroup, node.registryName, runRequest); + const run = await azureRegistryClient.registries.beginScheduleRunAndWait(resourceGroup, registryItem.label, runRequest); ext.outputChannel.info(vscode.l10n.t('Scheduled run {0}', run.runId)); - void streamLogs(context, node, run); + void streamLogs(context, registryItem, run); // function returns the AcrRun info - return async () => client.runs.get(node.resourceGroup, node.registryName, run.runId); + return async () => azureRegistryClient.runs.get(resourceGroup, registryItem.label, run.runId); } finally { if (await fse.pathExists(tarFilePath)) { await fse.unlink(tarFilePath); @@ -155,8 +162,10 @@ async function uploadSourceCode(client: ContainerRegistryManagementClient, regis const blobCheckInterval = 1000; const maxBlobChecks = 30; -async function streamLogs(context: IActionContext, node: AzureRegistryTreeItem, run: AcrRun): Promise { - const result = await (await node.getClient(context)).runs.getLogSasUrl(node.resourceGroup, node.registryName, run.runId); +async function streamLogs(context: IActionContext, registryItem: AzureRegistryItem, run: AcrRun): Promise { + const azureRegistryClient = await createAzureContainerRegistryClient(registryItem.subscription); + const resourceGroup = getResourceGroupFromId(registryItem.id); + const result = await azureRegistryClient.runs.getLogSasUrl(resourceGroup, registryItem.label, run.runId); const storageBlob = await getStorageBlob(); const blobClient = new storageBlob.BlobClient(nonNullProp(result, 'logLink')); diff --git a/src/commands/registries/azure/tasks/viewAzureTaskLogs.ts b/src/commands/registries/azure/tasks/viewAzureTaskLogs.ts deleted file mode 100644 index 4ed044abd4..0000000000 --- a/src/commands/registries/azure/tasks/viewAzureTaskLogs.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IActionContext, nonNullProp, openReadOnlyContent } from "@microsoft/vscode-azext-utils"; -import { l10n } from 'vscode'; -import { ext } from "../../../../extensionVariables"; -import { AzureTaskRunTreeItem } from "../../../../tree/registries/azure/AzureTaskRunTreeItem"; -import { bufferToString } from "../../../../utils/execAsync"; -import { getStorageBlob } from "../../../../utils/lazyPackages"; - -export async function viewAzureTaskLogs(context: IActionContext, node?: AzureTaskRunTreeItem): Promise { - if (!node) { - node = await ext.registriesTree.showTreeItemPicker(AzureTaskRunTreeItem.contextValue, context); - } - - const registryTI = node.parent.parent.parent; - await node.runWithTemporaryDescription(context, l10n.t('Retrieving logs...'), async () => { - const result = await (await registryTI.getClient(context)).runs.getLogSasUrl(registryTI.resourceGroup, registryTI.registryName, node.runId); - - const storageBlob = await getStorageBlob(); - const blobClient = new storageBlob.BlobClient(nonNullProp(result, 'logLink')); - const contentBuffer = await blobClient.downloadToBuffer(); - const content = bufferToString(contentBuffer); - - await openReadOnlyContent(node, content, '.log'); - }); -} diff --git a/src/commands/registries/azure/untagAzureImage.ts b/src/commands/registries/azure/untagAzureImage.ts index 4454bf5635..285ab301bb 100644 --- a/src/commands/registries/azure/untagAzureImage.ts +++ b/src/commands/registries/azure/untagAzureImage.ts @@ -6,31 +6,32 @@ import { IActionContext } from "@microsoft/vscode-azext-utils"; import { l10n, ProgressLocation, window } from "vscode"; import { ext } from "../../../extensionVariables"; -import { registryExpectedContextValues } from "../../../tree/registries/registryContextValues"; -import { RemoteTagTreeItem } from "../../../tree/registries/RemoteTagTreeItem"; -import { registryRequest } from "../../../utils/registryRequestUtils"; +import { AzureRegistryDataProvider, AzureTag } from "../../../tree/registries/Azure/AzureRegistryDataProvider"; +import { getFullImageNameFromRegistryTagItem } from "../../../tree/registries/registryTreeUtils"; +import { UnifiedRegistryItem } from "../../../tree/registries/UnifiedRegistryTreeDataProvider"; +import { registryExperience } from "../../../utils/registryExperience"; -export async function untagAzureImage(context: IActionContext, node?: RemoteTagTreeItem): Promise { +export async function untagAzureImage(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.azure.tag, { - ...context, - suppressCreatePick: true, - noItemFoundErrorMessage: l10n.t('No images are available to untag') + // we can't pass in the azure tree provider because it's not a UnifiedRegistryItem and we need the provider to untag + node = await registryExperience(context, { + registryFilter: { include: [ext.azureRegistryDataProvider.label] }, + contextValueFilter: { include: /commontag/i }, }); } - const confirmUntag: string = l10n.t('Are you sure you want to untag image "{0}"? This does not delete the manifest referenced by the tag.', node.repoNameAndTag); + const fullTag = getFullImageNameFromRegistryTagItem(node.wrappedItem); + const confirmUntag: string = l10n.t('Are you sure you want to untag image "{0}"? This does not delete the manifest referenced by the tag.', fullTag); // no need to check result - cancel will throw a UserCancelledError await context.ui.showWarningMessage(confirmUntag, { modal: true }, { title: "Untag" }); - const untagging = l10n.t('Untagging image "{0}"...', node.repoNameAndTag); - const repoTI = node.parent; + const untagging = l10n.t('Untagging image "{0}"...', fullTag); await window.withProgress({ location: ProgressLocation.Notification, title: untagging }, async () => { - await registryRequest(repoTI, 'DELETE', `v2/_acr/${repoTI.repoName}/tags/${node.tag}`); - await repoTI.refresh(context); + const provider = node.provider as unknown as AzureRegistryDataProvider; + await provider.untagImage(node.wrappedItem); }); // don't wait - /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ - window.showInformationMessage(l10n.t('Successfully untagged image "{0}".', node.repoNameAndTag)); + void ext.registriesTree.refresh(); + void window.showInformationMessage(l10n.t('Successfully untagged image "{0}".', fullTag)); } diff --git a/src/commands/registries/azure/viewAzureProperties.ts b/src/commands/registries/azure/viewAzureProperties.ts index 67804732ca..298dfdf74e 100644 --- a/src/commands/registries/azure/viewAzureProperties.ts +++ b/src/commands/registries/azure/viewAzureProperties.ts @@ -5,15 +5,18 @@ import { IActionContext, openReadOnlyJson } from "@microsoft/vscode-azext-utils"; import { ext } from "../../../extensionVariables"; -import { AzureRegistryTreeItem } from "../../../tree/registries/azure/AzureRegistryTreeItem"; -import { AzureTaskRunTreeItem } from "../../../tree/registries/azure/AzureTaskRunTreeItem"; -import { AzureTaskTreeItem } from "../../../tree/registries/azure/AzureTaskTreeItem"; -import { registryExpectedContextValues } from "../../../tree/registries/registryContextValues"; +import { AzureRegistry } from "../../../tree/registries/Azure/AzureRegistryDataProvider"; +import { UnifiedRegistryItem } from "../../../tree/registries/UnifiedRegistryTreeDataProvider"; +import { registryExperience } from "../../../utils/registryExperience"; -export async function viewAzureProperties(context: IActionContext, node?: AzureRegistryTreeItem | AzureTaskTreeItem | AzureTaskRunTreeItem): Promise { +export async function viewAzureProperties(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.azure.registry, context); + node = await registryExperience(context, { + registryFilter: { include: [ext.azureRegistryDataProvider.label] }, + contextValueFilter: { include: /commonregistry/i }, + }); } - await openReadOnlyJson(node, node.properties); + const registryItem = node.wrappedItem; + await openReadOnlyJson({ label: registryItem.label, fullId: registryItem.id }, registryItem.registryProperties); } diff --git a/src/commands/registries/connectRegistry.ts b/src/commands/registries/connectRegistry.ts index 93b747c7d8..dd4067a106 100644 --- a/src/commands/registries/connectRegistry.ts +++ b/src/commands/registries/connectRegistry.ts @@ -3,9 +3,8 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionContext } from "@microsoft/vscode-azext-utils"; import { ext } from "../../extensionVariables"; -export async function connectRegistry(context: IActionContext): Promise { - await ext.registriesRoot.connectRegistry(context); +export async function connectRegistry(): Promise { + await ext.registriesRoot.connectRegistryProvider(); } diff --git a/src/commands/registries/copyRemoteFullTag.ts b/src/commands/registries/copyRemoteFullTag.ts index 5e557dc43a..6cb63e1cfd 100644 --- a/src/commands/registries/copyRemoteFullTag.ts +++ b/src/commands/registries/copyRemoteFullTag.ts @@ -4,20 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext } from '@microsoft/vscode-azext-utils'; +import { CommonTag } from '@microsoft/vscode-docker-registries'; import * as vscode from 'vscode'; -import { ext } from '../../extensionVariables'; -import { registryExpectedContextValues } from '../../tree/registries/registryContextValues'; -import { RemoteTagTreeItem } from '../../tree/registries/RemoteTagTreeItem'; +import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { getFullImageNameFromRegistryTagItem } from '../../tree/registries/registryTreeUtils'; +import { registryExperience } from '../../utils/registryExperience'; -export async function copyRemoteFullTag(context: IActionContext, node?: RemoteTagTreeItem): Promise { +export async function copyRemoteFullTag(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker([registryExpectedContextValues.dockerV2.tag, registryExpectedContextValues.dockerHub.tag], { - ...context, - noItemFoundErrorMessage: vscode.l10n.t('No remote images are available to copy the full tag') - }); + node = await registryExperience(context, { contextValueFilter: { include: /commontag/i } }); } - // eslint-disable-next-line @typescript-eslint/no-floating-promises - // Don't wait - void vscode.env.clipboard.writeText(node.fullTag); - return node.fullTag; + const fullTag = getFullImageNameFromRegistryTagItem(node.wrappedItem); + void vscode.env.clipboard.writeText(fullTag); + return fullTag; } diff --git a/src/commands/registries/copyRemoteImageDigest.ts b/src/commands/registries/copyRemoteImageDigest.ts index f31ab24f2b..9411cca1f4 100644 --- a/src/commands/registries/copyRemoteImageDigest.ts +++ b/src/commands/registries/copyRemoteImageDigest.ts @@ -3,34 +3,23 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionContext, nonNullProp } from "@microsoft/vscode-azext-utils"; +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { CommonTag, RegistryV2DataProvider } from "@microsoft/vscode-docker-registries"; import * as vscode from "vscode"; import { ext } from "../../extensionVariables"; -import { AzureTaskRunTreeItem } from "../../tree/registries/azure/AzureTaskRunTreeItem"; -import { DockerV2TagTreeItem } from "../../tree/registries/dockerV2/DockerV2TagTreeItem"; -import { registryExpectedContextValues } from "../../tree/registries/registryContextValues"; +import { UnifiedRegistryItem } from "../../tree/registries/UnifiedRegistryTreeDataProvider"; +import { registryExperience } from "../../utils/registryExperience"; -export async function copyRemoteImageDigest(context: IActionContext, node?: DockerV2TagTreeItem | AzureTaskRunTreeItem): Promise { +export async function copyRemoteImageDigest(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.dockerV2.tag, { - ...context, - noItemFoundErrorMessage: vscode.l10n.t('No remote images are available to copy the digest') + node = await registryExperience(context, { + registryFilter: { exclude: [ext.dockerHubRegistryDataProvider.label] }, + contextValueFilter: { include: /commontag/i, }, }); } - let digest: string; - if (node instanceof AzureTaskRunTreeItem) { - if (node.outputImage) { - digest = nonNullProp(node.outputImage, 'digest'); - } else { - throw new Error(vscode.l10n.t('Failed to find output image for this task run.')); - } - } else { - await node.runWithTemporaryDescription(context, vscode.l10n.t('Getting digest...'), async () => { - digest = await (node).getDigest(); - }); - } + const v2DataProvider = node.provider as unknown as RegistryV2DataProvider; + const digest = await v2DataProvider.getImageDigest(node.wrappedItem); - /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ - vscode.env.clipboard.writeText(digest); + void vscode.env.clipboard.writeText(digest); } diff --git a/src/commands/registries/deleteRemoteImage.ts b/src/commands/registries/deleteRemoteImage.ts index ec938b1f68..10b430025a 100644 --- a/src/commands/registries/deleteRemoteImage.ts +++ b/src/commands/registries/deleteRemoteImage.ts @@ -3,35 +3,51 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DialogResponses, IActionContext } from '@microsoft/vscode-azext-utils'; -import { l10n, ProgressLocation, window } from 'vscode'; +import { DialogResponses, IActionContext, UserCancelledError, parseError } from '@microsoft/vscode-azext-utils'; +import { CommonRegistryDataProvider, CommonTag } from '@microsoft/vscode-docker-registries'; +import { ProgressLocation, l10n, window } from 'vscode'; import { ext } from '../../extensionVariables'; -import { DockerV2TagTreeItem } from '../../tree/registries/dockerV2/DockerV2TagTreeItem'; -import { registryExpectedContextValues } from '../../tree/registries/registryContextValues'; +import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { getImageNameFromRegistryTagItem } from '../../tree/registries/registryTreeUtils'; +import { registryExperience } from '../../utils/registryExperience'; -export async function deleteRemoteImage(context: IActionContext, node?: DockerV2TagTreeItem): Promise { +export async function deleteRemoteImage(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.dockerV2.tag, { - ...context, - suppressCreatePick: true, - noItemFoundErrorMessage: l10n.t('No remote images are available to delete') + node = await registryExperience(context, { + registryFilter: { exclude: [ext.githubRegistryDataProvider.label, ext.dockerHubRegistryDataProvider.label] }, + contextValueFilter: { include: /commontag/i }, }); } - const confirmDelete = l10n.t('Are you sure you want to delete image "{0}"? This will delete all images that have the same digest.', node.repoNameAndTag); + const provider = node.provider as unknown as CommonRegistryDataProvider; + if (typeof provider.deleteTag !== 'function') { + throw new Error(l10n.t('Deleting remote images is not supported on this registry.')); + } + + const tagName = getImageNameFromRegistryTagItem(node.wrappedItem); + const confirmDelete = l10n.t('Are you sure you want to delete image "{0}"? This will delete all images that have the same digest.', tagName); // no need to check result - cancel will throw a UserCancelledError await context.ui.showWarningMessage(confirmDelete, { modal: true }, DialogResponses.deleteResponse); - const repoTI = node.parent; - const deleting = l10n.t('Deleting image "{0}"...', node.repoNameAndTag); + const deleting = l10n.t('Deleting image "{0}"...', tagName); await window.withProgress({ location: ProgressLocation.Notification, title: deleting }, async () => { - await node.deleteTreeItem(context); + try { + await provider.deleteTag(node.wrappedItem); + } catch (error) { + const errorType: string = parseError(error).errorType.toLowerCase(); + if (errorType === '405' || errorType === 'unsupported') { + // Don't wait + // eslint-disable-next-line @typescript-eslint/no-floating-promises + context.ui.showWarningMessage('Deleting remote images is not supported on this registry. It may need to be enabled.', { learnMoreLink: 'https://aka.ms/AA7jsql' }); + throw new UserCancelledError(); + } else { + throw error; + } + } }); // Other tags that also matched the image may have been deleted, so refresh the whole repository - await repoTI.refresh(context); - const message = l10n.t('Successfully deleted image "{0}".', node.repoNameAndTag); // don't wait - /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ - window.showInformationMessage(message); + void ext.registriesTree.refresh(); + void window.showInformationMessage(l10n.t('Successfully deleted image "{0}".', tagName)); } diff --git a/src/commands/registries/disconnectRegistry.ts b/src/commands/registries/disconnectRegistry.ts index 8416735ef9..50a81d50c5 100644 --- a/src/commands/registries/disconnectRegistry.ts +++ b/src/commands/registries/disconnectRegistry.ts @@ -3,17 +3,16 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionContext, InvalidTreeItem } from "@microsoft/vscode-azext-utils"; +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { CommonRegistry } from "@microsoft/vscode-docker-registries"; import { ext } from "../../extensionVariables"; -import { ICachedRegistryProvider } from "../../tree/registries/ICachedRegistryProvider"; -import { IRegistryProviderTreeItem } from "../../tree/registries/IRegistryProviderTreeItem"; +import { UnifiedRegistryItem } from "../../tree/registries/UnifiedRegistryTreeDataProvider"; +import { registryExperience } from "../../utils/registryExperience"; -export async function disconnectRegistry(context: IActionContext, node?: InvalidTreeItem | IRegistryProviderTreeItem): Promise { - let cachedProvider: ICachedRegistryProvider | undefined; - if (node instanceof InvalidTreeItem) { - cachedProvider = node.data; - } else if (node) { - cachedProvider = node.cachedProvider; +export async function disconnectRegistry(context: IActionContext, node?: UnifiedRegistryItem): Promise { + if (!node) { + node = await registryExperience(context, { registryFilter: { exclude: [ext.genericRegistryV2DataProvider.label] } }); } - await ext.registriesRoot.disconnectRegistry(context, cachedProvider); + + await ext.registriesTree.disconnectRegistryProvider(node); } diff --git a/src/commands/registries/dockerHub/openDockerHubInBrowser.ts b/src/commands/registries/dockerHub/openDockerHubInBrowser.ts index 303d61f9ce..a3cc62af61 100644 --- a/src/commands/registries/dockerHub/openDockerHubInBrowser.ts +++ b/src/commands/registries/dockerHub/openDockerHubInBrowser.ts @@ -4,30 +4,32 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { CommonRegistryItem, isRegistry, isRepository, isTag } from "@microsoft/vscode-docker-registries"; import * as vscode from "vscode"; import { dockerHubUrl } from "../../../constants"; import { ext } from "../../../extensionVariables"; -import { DockerHubNamespaceTreeItem } from "../../../tree/registries/dockerHub/DockerHubNamespaceTreeItem"; -import { DockerHubRepositoryTreeItem } from "../../../tree/registries/dockerHub/DockerHubRepositoryTreeItem"; -import { registryExpectedContextValues } from "../../../tree/registries/registryContextValues"; -import { RemoteTagTreeItem } from "../../../tree/registries/RemoteTagTreeItem"; +import { UnifiedRegistryItem } from "../../../tree/registries/UnifiedRegistryTreeDataProvider"; +import { registryExperience } from "../../../utils/registryExperience"; -export async function openDockerHubInBrowser(context: IActionContext, node?: DockerHubNamespaceTreeItem | DockerHubRepositoryTreeItem | RemoteTagTreeItem): Promise { +export async function openDockerHubInBrowser(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.dockerHub.registry, { - ...context, - noItemFoundErrorMessage: vscode.l10n.t('No Docker Hub registries available to browse') + node = await registryExperience(context, { + registryFilter: { include: [ext.dockerHubRegistryDataProvider.label] }, + contextValueFilter: { include: [/commonregistry/i] }, }); } let url = dockerHubUrl; - if (node instanceof DockerHubNamespaceTreeItem) { - url += `u/${node.namespace}`; - } else if (node instanceof DockerHubRepositoryTreeItem) { - url += `r/${node.parent.namespace}/${node.repoName}`; + const dockerHubItem = node.wrappedItem; + + if (isRegistry(dockerHubItem)) { + url = `${url}u/${dockerHubItem.label}`; + } else if (isRepository(dockerHubItem)) { + url = `${url}r/${dockerHubItem.parent.label}/${dockerHubItem.label}`; + } else if (isTag(dockerHubItem)) { + url = `${url}r/${dockerHubItem.parent.parent.label}/${dockerHubItem.parent.label}/tags`; } else { - const repoTI = node.parent; - url += `r/${repoTI.parent.namespace}/${repoTI.repoName}/tags`; + throw new Error(`Unexpected node type ${dockerHubItem.additionalContextValues || ''}`); } await vscode.env.openExternal(vscode.Uri.parse(url)); diff --git a/src/commands/registries/genericV2/addTrackedGenericV2Registry.ts b/src/commands/registries/genericV2/addTrackedGenericV2Registry.ts new file mode 100644 index 0000000000..91baad9b44 --- /dev/null +++ b/src/commands/registries/genericV2/addTrackedGenericV2Registry.ts @@ -0,0 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { ext } from "../../../extensionVariables"; +import { UnifiedRegistryItem } from "../../../tree/registries/UnifiedRegistryTreeDataProvider"; + +export async function addTrackedGenericV2Registry(context: IActionContext, node?: UnifiedRegistryItem): Promise { + await ext.genericRegistryV2DataProvider.addTrackedRegistry(); + // don't wait + void ext.registriesTree.refresh(); +} diff --git a/src/commands/registries/genericV2/removeTrackedGenericV2Registry.ts b/src/commands/registries/genericV2/removeTrackedGenericV2Registry.ts new file mode 100644 index 0000000000..93dbe84464 --- /dev/null +++ b/src/commands/registries/genericV2/removeTrackedGenericV2Registry.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { V2Registry } from "@microsoft/vscode-docker-registries"; +import { ext } from "../../../extensionVariables"; +import { UnifiedRegistryItem } from "../../../tree/registries/UnifiedRegistryTreeDataProvider"; +import { registryExperience } from "../../../utils/registryExperience"; + +export async function removeTrackedGenericV2Registry(context: IActionContext, node?: UnifiedRegistryItem): Promise { + if (!node) { + node = await registryExperience(context, { + registryFilter: { include: [ext.genericRegistryV2DataProvider.label] }, + contextValueFilter: { include: /commonregistry/i }, + }); + } + + await ext.genericRegistryV2DataProvider.removeTrackedRegistry(node.wrappedItem); + + // remove the provider if it's the last one + if ((await ext.genericRegistryV2DataProvider.getRegistries(node.parent.wrappedItem)).length === 0) { + await ext.registriesTree.disconnectRegistryProvider(node.parent); + } + + // don't wait + void ext.registriesTree.refresh(); +} diff --git a/src/commands/registries/logInToDockerCli.ts b/src/commands/registries/logInToDockerCli.ts index b4c367e9e3..2f30e6f8ef 100644 --- a/src/commands/registries/logInToDockerCli.ts +++ b/src/commands/registries/logInToDockerCli.ts @@ -4,32 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext, parseError } from '@microsoft/vscode-azext-utils'; +import { CommonRegistry } from '@microsoft/vscode-docker-registries'; import * as stream from 'stream'; import * as vscode from 'vscode'; -import { NULL_GUID } from '../../constants'; import { ext } from '../../extensionVariables'; -import { registryExpectedContextValues } from '../../tree/registries/registryContextValues'; -import { RegistryTreeItemBase } from '../../tree/registries/RegistryTreeItemBase'; +import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { registryExperience } from '../../utils/registryExperience'; -export async function logInToDockerCli(context: IActionContext, node?: RegistryTreeItemBase): Promise { +export async function logInToDockerCli(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.all.registry, context); + node = await registryExperience(context, { contextValueFilter: { include: /commonregistry/i } }); } - const creds = await node.getDockerCliCredentials(); - const auth: { username?: string, password?: string, token?: string } = creds.auth || {}; - let username: string | undefined; - let password: string | undefined; - if (auth.token) { - username = NULL_GUID; - password = auth.token; - } else if (auth.password) { - username = auth.username; - password = auth.password; - } + const creds = await node.provider?.getLoginInformation?.(node.wrappedItem); + const username = creds?.username; + const secret = creds?.secret; + const server = creds?.server; - if (!username || !password) { - ext.outputChannel.warn(vscode.l10n.t('Skipping login for "{0}" because it does not require authentication.', creds.registryPath)); + if (!username || !secret) { + ext.outputChannel.warn(vscode.l10n.t('Skipping login for "{0}" because it does not require authentication.', node.provider.label)); } else { const progressOptions: vscode.ProgressOptions = { location: vscode.ProgressLocation.Notification, @@ -42,10 +35,10 @@ export async function logInToDockerCli(context: IActionContext, node?: RegistryT client => client.login({ username: username, passwordStdIn: true, - registry: creds.registryPath, + registry: server }), { - stdInPipe: stream.Readable.from(password), + stdInPipe: stream.Readable.from(secret), } ); ext.outputChannel.info('Login succeeded.'); diff --git a/src/commands/registries/logOutOfDockerCli.ts b/src/commands/registries/logOutOfDockerCli.ts index 4e828f109e..dd9e774798 100644 --- a/src/commands/registries/logOutOfDockerCli.ts +++ b/src/commands/registries/logOutOfDockerCli.ts @@ -4,17 +4,21 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext } from '@microsoft/vscode-azext-utils'; +import { CommonRegistry } from '@microsoft/vscode-docker-registries'; +import { l10n } from 'vscode'; import { ext } from '../../extensionVariables'; import { TaskCommandRunnerFactory } from '../../runtimes/runners/TaskCommandRunnerFactory'; -import { registryExpectedContextValues } from '../../tree/registries/registryContextValues'; -import { RegistryTreeItemBase } from '../../tree/registries/RegistryTreeItemBase'; +import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { registryExperience } from '../../utils/registryExperience'; -export async function logOutOfDockerCli(context: IActionContext, node?: RegistryTreeItemBase): Promise { +export async function logOutOfDockerCli(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.all.registry, context); + node = await registryExperience(context, { contextValueFilter: { include: /commonregistry/i } }); + } + const serverUrl = (await node.provider.getLoginInformation?.(node.wrappedItem))?.server; + if (!serverUrl) { + throw new Error(l10n.t('Unable to get server URL')); } - - const creds = await node.getDockerCliCredentials(); const client = await ext.runtimeManager.getClient(); const taskCRF = new TaskCommandRunnerFactory( @@ -24,6 +28,6 @@ export async function logOutOfDockerCli(context: IActionContext, node?: Registry ); await taskCRF.getCommandRunner()( - client.logout({ registry: creds.registryPath }) + client.logout({ registry: serverUrl }), ); } diff --git a/src/commands/registries/pullImages.ts b/src/commands/registries/pullImages.ts index 47dd2eda37..7e21b5447e 100644 --- a/src/commands/registries/pullImages.ts +++ b/src/commands/registries/pullImages.ts @@ -4,32 +4,33 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext } from '@microsoft/vscode-azext-utils'; +import { CommonRegistry, CommonRepository, CommonTag } from '@microsoft/vscode-docker-registries/lib/clients/Common/models'; import { ext } from '../../extensionVariables'; import { TaskCommandRunnerFactory } from '../../runtimes/runners/TaskCommandRunnerFactory'; -import { registryExpectedContextValues } from '../../tree/registries/registryContextValues'; -import { RegistryTreeItemBase } from '../../tree/registries/RegistryTreeItemBase'; -import { RemoteRepositoryTreeItemBase } from '../../tree/registries/RemoteRepositoryTreeItemBase'; -import { RemoteTagTreeItem } from '../../tree/registries/RemoteTagTreeItem'; +import { UnifiedRegistryItem } from '../../tree/registries/UnifiedRegistryTreeDataProvider'; +import { getFullImageNameFromRegistryTagItem, getFullRepositoryNameFromRepositoryItem } from '../../tree/registries/registryTreeUtils'; +import { registryExperience } from '../../utils/registryExperience'; import { logInToDockerCli } from './logInToDockerCli'; -export async function pullRepository(context: IActionContext, node?: RemoteRepositoryTreeItemBase): Promise { +export async function pullRepository(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.all.repository, context); + node = await registryExperience(context, { contextValueFilter: { include: /commonrepository/i } }); } - await pullImages(context, node.parent, node.repoName, true); + await pullImages(context, node.parent, getFullRepositoryNameFromRepositoryItem(node.wrappedItem), true); } -export async function pullImageFromRepository(context: IActionContext, node?: RemoteTagTreeItem): Promise { +export async function pullImageFromRepository(context: IActionContext, node?: UnifiedRegistryItem): Promise { if (!node) { - node = await ext.registriesTree.showTreeItemPicker(registryExpectedContextValues.all.tag, context); + node = await registryExperience(context, { contextValueFilter: { include: /commontag/i } }); } - await pullImages(context, node.parent.parent, node.repoNameAndTag, false); + await pullImages(context, node.parent.parent, getFullImageNameFromRegistryTagItem(node.wrappedItem), false); } -async function pullImages(context: IActionContext, node: RegistryTreeItemBase, imageRequest: string, allTags: boolean): Promise { - await logInToDockerCli(context, node); +async function pullImages(context: IActionContext, node: UnifiedRegistryItem, imageRequest: string, allTags: boolean): Promise { + const registryNode = node as UnifiedRegistryItem; + await logInToDockerCli(context, registryNode); const client = await ext.runtimeManager.getClient(); const taskCRF = new TaskCommandRunnerFactory({ @@ -39,7 +40,7 @@ async function pullImages(context: IActionContext, node: RegistryTreeItemBase, i await taskCRF.getCommandRunner()( client.pullImage( { - imageRef: `${node.baseImagePath}/${imageRequest}`, + imageRef: imageRequest, allTags: allTags, } ) diff --git a/src/commands/registries/reconnectRegistry.ts b/src/commands/registries/reconnectRegistry.ts index 31e0c79be7..258d5027fb 100644 --- a/src/commands/registries/reconnectRegistry.ts +++ b/src/commands/registries/reconnectRegistry.ts @@ -4,16 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import { IActionContext } from "@microsoft/vscode-azext-utils"; +import { RegistryConnectError, V2Registry, isGenericV2Registry } from "@microsoft/vscode-docker-registries"; import { l10n } from 'vscode'; import { ext } from "../../extensionVariables"; -import { RegistryConnectErrorTreeItem } from "../../tree/registries/RegistryConnectErrorTreeItem"; +import { UnifiedRegistryItem } from "../../tree/registries/UnifiedRegistryTreeDataProvider"; -export async function reconnectRegistry(context: IActionContext, node?: RegistryConnectErrorTreeItem): Promise { - if (!node?.cachedProvider || !node?.provider) { +export async function reconnectRegistry(context: IActionContext, node?: UnifiedRegistryItem): Promise { + if (!node?.provider || !node?.wrappedItem || !node?.parent) { // This is not expected ever, so we'll throw an error which can be bubbled up to a Report Issue if it does throw new Error(l10n.t('Unable to determine provider to re-enter credentials. Please disconnect and connect again.')); } - await ext.registriesRoot.disconnectRegistry(context, node.cachedProvider); - await ext.registriesRoot.connectRegistry(context, node.provider, node.url); + if (isGenericV2Registry(node.parent.wrappedItem)) { + await ext.genericRegistryV2DataProvider.removeTrackedRegistry(node.parent.wrappedItem as V2Registry); + await ext.genericRegistryV2DataProvider.addTrackedRegistry(); + } else { + await ext.registriesTree.disconnectRegistryProvider(node.parent); + await ext.registriesRoot.connectRegistryProvider(node.provider); + } } diff --git a/src/extension.ts b/src/extension.ts index 8fb384f336..3f814e2fdc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -25,7 +25,6 @@ import { registerListeners } from './telemetry/registerListeners'; import { registerTrees } from './tree/registerTrees'; import { AlternateYamlLanguageServiceClientFeature } from './utils/AlternateYamlLanguageServiceClientFeature'; import { AzExtLogOutputChannelWrapper } from './utils/AzExtLogOutputChannelWrapper'; -import { AzureAccountExtensionListener } from './utils/AzureAccountExtensionListener'; import { logDockerEnvironment, logSystemInfo } from './utils/diagnostics'; import { DocumentSettingsClientFeature } from './utils/DocumentSettingsClientFeature'; import { migrateOldEnvironmentSettingsIfNeeded } from './utils/migrateOldEnvironmentSettingsIfNeeded'; @@ -136,13 +135,16 @@ export async function activateInternal(ctx: vscode.ExtensionContext, perfStats: // Don't wait void migrateOldEnvironmentSettingsIfNeeded(); + // Call command to activate registry provider extensions + // Don't wait + void vscode.commands.executeCommand('vscode-docker.activateRegistryProviders'); + return new DockerExtensionApi(ctx); } export async function deactivateInternal(ctx: vscode.ExtensionContext): Promise { await callWithTelemetryAndErrorHandling('docker.deactivate', async (activateContext: IActionContext) => { activateContext.telemetry.properties.isActivationEvent = 'true'; - AzureAccountExtensionListener.dispose(); await Promise.all([ dockerfileLanguageClient.stop(), diff --git a/src/extensionVariables.ts b/src/extensionVariables.ts index e3c403caea..487ea372d3 100644 --- a/src/extensionVariables.ts +++ b/src/extensionVariables.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AzExtTreeDataProvider, AzExtTreeItem, IExperimentationServiceAdapter } from '@microsoft/vscode-azext-utils'; +import { DockerHubRegistryDataProvider, GenericRegistryV2DataProvider, GitHubRegistryDataProvider } from '@microsoft/vscode-docker-registries'; import { ExtensionContext, StatusBarItem, TreeView } from 'vscode'; import { ContainerRuntimeManager } from './runtimes/ContainerRuntimeManager'; import { OrchestratorRuntimeManager } from './runtimes/OrchestratorRuntimeManager'; @@ -13,7 +14,8 @@ 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 { RegistriesTreeItem } from './tree/registries/RegistriesTreeItem'; +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'; @@ -45,9 +47,13 @@ export namespace ext { export const prefix: string = 'docker'; - export let registriesTree: AzExtTreeDataProvider; - export let registriesTreeView: TreeView; - export let registriesRoot: RegistriesTreeItem; + export let registriesTree: UnifiedRegistryTreeDataProvider; + export let registriesTreeView: TreeView>; + export let registriesRoot: UnifiedRegistryTreeDataProvider; + export let genericRegistryV2DataProvider: GenericRegistryV2DataProvider; + export let azureRegistryDataProvider: AzureRegistryDataProvider; + export let dockerHubRegistryDataProvider: DockerHubRegistryDataProvider; + export let githubRegistryDataProvider: GitHubRegistryDataProvider; export let volumesTree: AzExtTreeDataProvider; export let volumesTreeView: TreeView; diff --git a/src/tree/RefreshManager.ts b/src/tree/RefreshManager.ts index f3ab4226cd..502ac49604 100644 --- a/src/tree/RefreshManager.ts +++ b/src/tree/RefreshManager.ts @@ -301,7 +301,7 @@ export class RefreshManager extends vscode.Disposable { callback = () => ext.networksRoot.refresh(context); break; case 'registries': - callback = () => ext.registriesRoot.refresh(context); + callback = () => ext.registriesRoot.refresh(); break; case 'volumes': callback = () => ext.volumesRoot.refresh(context); diff --git a/src/tree/registerTrees.ts b/src/tree/registerTrees.ts index c9f53f51a3..d29b58b7b3 100644 --- a/src/tree/registerTrees.ts +++ b/src/tree/registerTrees.ts @@ -3,18 +3,20 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as vscode from 'vscode'; import { AzExtTreeDataProvider, AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; +import { DockerHubRegistryDataProvider, GenericRegistryV2DataProvider, GitHubRegistryDataProvider } from "@microsoft/vscode-docker-registries"; +import * as vscode from 'vscode'; import { registerCommand } from '../commands/registerCommands'; import { ext } from '../extensionVariables'; +import { OpenUrlTreeItem } from './OpenUrlTreeItem'; +import { RefreshManager } from './RefreshManager'; import { ContainersTreeItem } from './containers/ContainersTreeItem'; import { ContextsTreeItem } from './contexts/ContextsTreeItem'; import { HelpsTreeItem } from './help/HelpsTreeItem'; import { ImagesTreeItem } from "./images/ImagesTreeItem"; import { NetworksTreeItem } from "./networks/NetworksTreeItem"; -import { OpenUrlTreeItem } from './OpenUrlTreeItem'; -import { RefreshManager } from './RefreshManager'; -import { RegistriesTreeItem } from "./registries/RegistriesTreeItem"; +import { AzureRegistryDataProvider } from "./registries/Azure/AzureRegistryDataProvider"; +import { UnifiedRegistryTreeDataProvider } from "./registries/UnifiedRegistryTreeDataProvider"; import { VolumesTreeItem } from "./volumes/VolumesTreeItem"; export function registerTrees(): void { @@ -42,13 +44,22 @@ export function registerTrees(): void { /* eslint-disable-next-line @typescript-eslint/promise-function-async */ registerCommand(imagesLoadMore, (context: IActionContext, node: AzExtTreeItem) => ext.imagesTree.loadMore(node, context)); - ext.registriesRoot = new RegistriesTreeItem(); - const registriesLoadMore = 'vscode-docker.registries.loadMore'; - ext.registriesTree = new AzExtTreeDataProvider(ext.registriesRoot, registriesLoadMore); - ext.registriesTreeView = vscode.window.createTreeView('dockerRegistries', { treeDataProvider: ext.registriesTree, showCollapseAll: true, canSelectMany: false }); - ext.context.subscriptions.push(ext.registriesTreeView); - /* eslint-disable-next-line @typescript-eslint/promise-function-async */ - registerCommand(registriesLoadMore, (context: IActionContext, node: AzExtTreeItem) => ext.registriesTree.loadMore(node, context)); + const urtdp = new UnifiedRegistryTreeDataProvider(ext.context.globalState); + const genericRegistryV2DataProvider = new GenericRegistryV2DataProvider(ext.context); + const azureRegistryDataProvider = new AzureRegistryDataProvider(ext.context); + const dockerHubRegistryDataProvider = new DockerHubRegistryDataProvider(ext.context); + const githubRegistryDataProvider = new GitHubRegistryDataProvider(ext.context); + urtdp.registerProvider(githubRegistryDataProvider); + urtdp.registerProvider(dockerHubRegistryDataProvider); + 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.dockerHubRegistryDataProvider = dockerHubRegistryDataProvider; + ext.githubRegistryDataProvider = githubRegistryDataProvider; ext.volumesRoot = new VolumesTreeItem(undefined); const volumesLoadMore = 'vscode-docker.volumes.loadMore'; diff --git a/src/tree/registries/Azure/ACROAuthProvider.ts b/src/tree/registries/Azure/ACROAuthProvider.ts new file mode 100644 index 0000000000..a2a47888c3 --- /dev/null +++ b/src/tree/registries/Azure/ACROAuthProvider.ts @@ -0,0 +1,124 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureSubscription } from '@microsoft/vscode-azext-azureauth'; +import { LoginInformation, httpRequest } from '@microsoft/vscode-docker-registries'; +import { AuthenticationProvider } from "@microsoft/vscode-docker-registries/"; +import * as vscode from 'vscode'; +import { NULL_GUID } from '../../../constants'; + +export class ACROAuthProvider implements AuthenticationProvider { + private refreshTokenCache = new Map(); + + public constructor(private readonly registryUri: vscode.Uri, private readonly subscription: AzureSubscription) { } + + public async getSession(scopes: string[], options?: vscode.AuthenticationGetSessionOptions): Promise { + const refreshToken = await this.getRefreshToken(options); + + const oauthToken = await this.getOAuthTokenFromRefreshToken(refreshToken, this.registryUri, scopes.join(' '), this.subscription); + const { sub, jti } = this.parseToken(oauthToken); + + return { + id: jti, + type: 'Bearer', + accessToken: oauthToken, + account: { + label: sub, + id: sub, + }, + scopes: scopes, + }; + } + + public async getLoginInformation(options?: vscode.AuthenticationGetSessionOptions): Promise { + const refreshToken = await this.getRefreshToken(options); + return { + username: NULL_GUID, + secret: refreshToken, + server: this.registryUri.toString(), + }; + } + + private parseToken(accessToken: string): { sub: string, jti: string } { + const tokenParts = accessToken.split('.'); + const tokenBody = JSON.parse(Buffer.from(tokenParts[1], 'base64').toString('utf8')); + return { + sub: tokenBody.sub, + jti: tokenBody.jti, + }; + } + + private async getOAuthTokenFromRefreshToken(refreshToken: string, registryUri: vscode.Uri, scopes: string, subscription: AzureSubscription): Promise { + const requestUrl = registryUri.with({ path: '/oauth2/token' }); + + const requestBody = new URLSearchParams({ + /* eslint-disable @typescript-eslint/naming-convention */ + grant_type: 'refresh_token', + refresh_token: refreshToken, + service: registryUri.authority, + scope: scopes, + }); + + const response = await httpRequest<{ access_token: string }>(requestUrl.toString(), { + method: 'POST', + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'content-type': 'application/x-www-form-urlencoded' + }, + body: requestBody, + }); + + return (await response.json()).access_token; + } + + private async getRefreshTokenFromAccessToken(accessToken: string, registryUri: vscode.Uri, subscription: AzureSubscription): Promise { + const requestUrl = registryUri.with({ path: '/oauth2/exchange' }); + + const requestBody = new URLSearchParams({ + /* eslint-disable @typescript-eslint/naming-convention */ + grant_type: 'access_token', + access_token: accessToken, + service: registryUri.authority, + tenant: subscription.tenantId, + /* eslint-enable @typescript-eslint/naming-convention */ + }); + + // eslint-disable-next-line @typescript-eslint/naming-convention + const response = await httpRequest<{ refresh_token: string }>(requestUrl.toString(), { + method: 'POST', + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'content-type': 'application/x-www-form-urlencoded' + }, + body: requestBody, + }); + + return (await response.json()).refresh_token; + } + + private async getAccessToken(subscription: AzureSubscription): Promise { + // Registry scopes, i.e. those passed to `getSession()`, are not valid for acquiring this + // access token--instead, those only need to be passed to `getOAuthTokenFromRefreshToken()` + const token = await subscription.credential.getToken([]); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return token!.token; + } + + private async getRefreshToken(options?: vscode.AuthenticationGetSessionOptions): Promise { + const accessToken = await this.getAccessToken(this.subscription); + const registryString = this.registryUri.toString(); + + let refreshToken: string; + if (!options?.forceNewSession && this.refreshTokenCache.has(registryString)) { + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + refreshToken = this.refreshTokenCache.get(registryString)!; + } else { + refreshToken = await this.getRefreshTokenFromAccessToken(accessToken, this.registryUri, this.subscription); + this.refreshTokenCache.set(registryString, refreshToken); + } + + return refreshToken; + } +} diff --git a/src/tree/registries/Azure/AzureRegistryDataProvider.ts b/src/tree/registries/Azure/AzureRegistryDataProvider.ts new file mode 100644 index 0000000000..494eec04c8 --- /dev/null +++ b/src/tree/registries/Azure/AzureRegistryDataProvider.ts @@ -0,0 +1,208 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { Registry as AcrRegistry, RegistryListCredentialsResult } from '@azure/arm-containerregistry'; +import { AzureSubscription, VSCodeAzureSubscriptionProvider } from '@microsoft/vscode-azext-azureauth'; +import { RegistryV2DataProvider, V2Registry, V2RegistryItem, V2Repository, V2Tag, getContextValue, registryV2Request } from '@microsoft/vscode-docker-registries'; +import { CommonRegistryItem, isRegistry, isRegistryRoot, isRepository, isTag } from '@microsoft/vscode-docker-registries/lib/clients/Common/models'; +import * as vscode from 'vscode'; +import { createAzureContainerRegistryClient, getResourceGroupFromId } from '../../../utils/azureUtils'; +import { ACROAuthProvider } from './ACROAuthProvider'; + +export interface AzureRegistryItem extends V2RegistryItem { + readonly subscription: AzureSubscription; + readonly id: string; +} + +export interface AzureSubscriptionRegistryItem extends CommonRegistryItem { + readonly subscription: AzureSubscription; + readonly type: 'azuresubscription'; +} + +export type AzureRegistry = V2Registry & AzureRegistryItem & { + readonly registryProperties: AcrRegistry; +}; + +export type AzureRepository = V2Repository; + +export type AzureTag = V2Tag; + +export function isAzureSubscriptionRegistryItem(item: unknown): item is AzureSubscriptionRegistryItem { + return !!item && typeof item === 'object' && (item as AzureSubscriptionRegistryItem).type === 'azuresubscription'; +} + +export function isAzureRegistry(item: unknown): item is AzureRegistry { + return isRegistry(item) && item.additionalContextValues?.includes('azure'); +} + +export function isAzureRepository(item: unknown): item is AzureRepository { + return isRepository(item) && item.additionalContextValues?.includes('azure'); +} + +export function isAzureTag(item: unknown): item is AzureTag { + return isTag(item) && item.additionalContextValues?.includes('azure'); +} + +export class AzureRegistryDataProvider extends RegistryV2DataProvider implements vscode.Disposable { + public readonly id = 'vscode-docker.azureContainerRegistry'; + public readonly label = vscode.l10n.t('Azure'); + public readonly iconPath = new vscode.ThemeIcon('azure'); + public readonly description = vscode.l10n.t('Azure Container Registry'); + + private readonly subscriptionProvider = new VSCodeAzureSubscriptionProvider(); + private readonly authenticationProviders = new Map(); // The tree items are too short-lived to store the associated auth provider so keep a cache + + public constructor(private readonly extensionContext: vscode.ExtensionContext) { + super(); + } + + public override async getChildren(element?: CommonRegistryItem | undefined): Promise { + if (isRegistryRoot(element)) { + if (!await this.subscriptionProvider.isSignedIn()) { + await this.subscriptionProvider.signIn(); + this.fireSoon(() => this.onDidChangeTreeDataEmitter.fire(element)); + return []; + } + + const subscriptions = await this.subscriptionProvider.getSubscriptions(); + + return subscriptions.map(sub => { + return { + parent: element, + label: sub.name, + type: 'azuresubscription', + subscription: sub, + additionalContextValues: ['azuresubscription'], + iconPath: vscode.Uri.joinPath(this.extensionContext.extensionUri, 'dist', 'node_modules', '@microsoft', 'vscode-azext-azureutils', 'resources', 'azureSubscription.svg'), + } as AzureSubscriptionRegistryItem; + }); + } else if (isAzureSubscriptionRegistryItem(element)) { + const registries = await this.getRegistries(element); + registries.forEach(registry => { + registry.additionalContextValues = [...(registry.additionalContextValues || []), 'azure']; + }); + return registries; + } else { + const children = await super.getChildren(element); + + if ((element as AzureRegistryItem)?.subscription) { + children.forEach(e => { + e.subscription = (element as AzureRegistryItem).subscription; + e.additionalContextValues = [...(e.additionalContextValues || []), 'azure']; + }); + } + + return children; + } + } + + public dispose(): void { + this.subscriptionProvider.dispose(); + } + + public override async getRegistries(subscriptionItem: CommonRegistryItem): Promise { + subscriptionItem = subscriptionItem as AzureSubscriptionRegistryItem; + + const acrClient = await createAzureContainerRegistryClient(subscriptionItem.subscription); + const registries: AcrRegistry[] = []; + + for await (const registry of acrClient.registries.list()) { + registries.push(registry); + } + + return registries.map(registry => { + return { + parent: subscriptionItem, + type: 'commonregistry', + baseUrl: vscode.Uri.parse(`https://${registry.loginServer}`), + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + label: registry.name!, + iconPath: vscode.Uri.joinPath(this.extensionContext.extensionUri, 'resources', 'azureRegistry.svg'), + subscription: subscriptionItem.subscription, + additionalContextValues: ['azureContainerRegistry'], + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + id: registry.id!, + registryProperties: registry + }; + }); + } + + public override getTreeItem(element: CommonRegistryItem): Promise { + if (isAzureSubscriptionRegistryItem(element)) { + return Promise.resolve({ + label: element.label, + collapsibleState: vscode.TreeItemCollapsibleState.Collapsed, + contextValue: getContextValue(element), + iconPath: element.iconPath, + }); + } else { + return super.getTreeItem(element); + } + } + + public async deleteRepository(item: AzureRepository): Promise { + const authenticationProvider = this.getAuthenticationProvider(item.parent as unknown as AzureRegistryItem); + const requestUrl = item.baseUrl.with({ path: `v2/_acr/${item.label}/repository` }); + const reponse = await registryV2Request({ + method: 'DELETE', + requestUri: requestUrl, + scopes: [`repository:${item.label}:delete`], + authenticationProvider: authenticationProvider, + }); + + if (!reponse.succeeded) { + throw new Error(`Failed to delete repository: ${reponse.statusText}`); + } + } + + public async deleteRegistry(item: AzureRegistry): Promise { + const client = await createAzureContainerRegistryClient(item.subscription); + const resourceGroup = getResourceGroupFromId(item.id); + await client.registries.beginDeleteAndWait(resourceGroup, item.label); + } + + public async untagImage(item: AzureTag): Promise { + const authenticationProvider = this.getAuthenticationProvider(item.parent.parent as unknown as AzureRegistryItem); + const requestUrl = item.baseUrl.with({ path: `v2/_acr/${item.parent.label}/tags/${item.label}` }); + const reponse = await registryV2Request({ + method: 'DELETE', + requestUri: requestUrl, + scopes: [`repository:${item.parent.label}:delete`], + authenticationProvider: authenticationProvider, + }); + + if (!reponse.succeeded) { + throw new Error(`Failed to delete tag: ${reponse.statusText}`); + } + } + + 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(); + + if (!this.authenticationProviders.has(registryString)) { + const provider = new ACROAuthProvider(item.baseUrl, item.subscription); + this.authenticationProviders.set(registryString, provider); + } + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + return this.authenticationProviders.get(registryString)!; + } + + private fireSoon(callback: () => void, delay: number = 5) { + const timeout = setTimeout(() => { + clearTimeout(timeout); + callback(); + }, delay); + } +} diff --git a/src/tree/registries/azure/createWizard/AzureRegistryCreateStep.ts b/src/tree/registries/Azure/createWizard/AzureRegistryCreateStep.ts similarity index 87% rename from src/tree/registries/azure/createWizard/AzureRegistryCreateStep.ts rename to src/tree/registries/Azure/createWizard/AzureRegistryCreateStep.ts index 077bb1aa84..43a330c51d 100644 --- a/src/tree/registries/azure/createWizard/AzureRegistryCreateStep.ts +++ b/src/tree/registries/Azure/createWizard/AzureRegistryCreateStep.ts @@ -5,9 +5,10 @@ import type { AzExtLocation } from '@microsoft/vscode-azext-azureutils'; import { AzureWizardExecuteStep, nonNullProp, parseError } from '@microsoft/vscode-azext-utils'; -import { l10n, Progress } from 'vscode'; +import { Progress, l10n } from 'vscode'; import { ext } from '../../../../extensionVariables'; -import { getArmContainerRegistry, getAzExtAzureUtils } from '../../../../utils/lazyPackages'; +import { createAzureContainerRegistryClient } from '../../../../utils/azureUtils'; +import { getAzExtAzureUtils } from '../../../../utils/lazyPackages'; import { IAzureRegistryWizardContext } from './IAzureRegistryWizardContext'; export class AzureRegistryCreateStep extends AzureWizardExecuteStep { @@ -16,9 +17,9 @@ export class AzureRegistryCreateStep extends AzureWizardExecuteStep): Promise { const newRegistryName = nonNullProp(context, 'newRegistryName'); + const client = await createAzureContainerRegistryClient(context.azureSubscription); + const azExtAzureUtils = await getAzExtAzureUtils(); - const armContainerRegistry = await getArmContainerRegistry(); - const client = azExtAzureUtils.createAzureClient(context, armContainerRegistry.ContainerRegistryManagementClient); const creating: string = l10n.t('Creating registry "{0}"...', newRegistryName); ext.outputChannel.info(creating); progress.report({ message: creating }); diff --git a/src/tree/registries/azure/createWizard/AzureRegistryNameStep.ts b/src/tree/registries/Azure/createWizard/AzureRegistryNameStep.ts similarity index 100% rename from src/tree/registries/azure/createWizard/AzureRegistryNameStep.ts rename to src/tree/registries/Azure/createWizard/AzureRegistryNameStep.ts diff --git a/src/tree/registries/azure/createWizard/AzureRegistrySkuStep.ts b/src/tree/registries/Azure/createWizard/AzureRegistrySkuStep.ts similarity index 100% rename from src/tree/registries/azure/createWizard/AzureRegistrySkuStep.ts rename to src/tree/registries/Azure/createWizard/AzureRegistrySkuStep.ts diff --git a/src/tree/registries/azure/createWizard/IAzureRegistryWizardContext.ts b/src/tree/registries/Azure/createWizard/IAzureRegistryWizardContext.ts similarity index 83% rename from src/tree/registries/azure/createWizard/IAzureRegistryWizardContext.ts rename to src/tree/registries/Azure/createWizard/IAzureRegistryWizardContext.ts index 7d0109f2b0..21e659baf7 100644 --- a/src/tree/registries/azure/createWizard/IAzureRegistryWizardContext.ts +++ b/src/tree/registries/Azure/createWizard/IAzureRegistryWizardContext.ts @@ -4,10 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import type { Registry as AcrRegistry, SkuName as AcrSkuName } from '@azure/arm-containerregistry'; // These are only dev-time imports so don't need to be lazy -import type { IResourceGroupWizardContext } from '@microsoft/vscode-azext-azureutils'; // These are only dev-time imports so don't need to be lazy +import type { AzureSubscription } from '@microsoft/vscode-azext-azureauth'; +import type { IResourceGroupWizardContext } from '@microsoft/vscode-azext-azureutils'; export interface IAzureRegistryWizardContext extends IResourceGroupWizardContext { newRegistryName?: string; newRegistrySku?: AcrSkuName; registry?: AcrRegistry; + readonly azureSubscription: AzureSubscription; } diff --git a/src/tree/registries/ConnectedRegistriesTreeItem.ts b/src/tree/registries/ConnectedRegistriesTreeItem.ts deleted file mode 100644 index 1a8a097bf7..0000000000 --- a/src/tree/registries/ConnectedRegistriesTreeItem.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem, AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; -import { l10n, ThemeIcon } from "vscode"; - -export class ConnectedRegistriesTreeItem extends AzExtParentTreeItem { - public contextValue: string = 'connectedRegistries'; - public childTypeLabel: string = 'registry'; - public label: string = l10n.t('Connected Registries'); - public children: AzExtTreeItem[] = []; - - public constructor(parent: AzExtParentTreeItem | undefined) { - super(parent); - this.iconPath = new ThemeIcon('link'); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - return this.children; - } - - public hasMoreChildrenImpl(): boolean { - return false; - } - - public isAncestorOfImpl(expectedContextValue: string | RegExp): boolean { - return this.children.some(c => c.isAncestorOfImpl && c.isAncestorOfImpl(expectedContextValue)); - } -} diff --git a/src/tree/registries/ICachedRegistryProvider.ts b/src/tree/registries/ICachedRegistryProvider.ts deleted file mode 100644 index aac276b037..0000000000 --- a/src/tree/registries/ICachedRegistryProvider.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { RegistryApi } from "./all/RegistryApi"; - -/** - * Basic _non-sensitive_ information that will be cached across sessions - */ -export interface ICachedRegistryProvider { - id: string; - api: RegistryApi; - url?: string; - username?: string; -} diff --git a/src/tree/registries/IRegistryProvider.ts b/src/tree/registries/IRegistryProvider.ts deleted file mode 100644 index bf1100dc81..0000000000 --- a/src/tree/registries/IRegistryProvider.ts +++ /dev/null @@ -1,68 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem } from "@microsoft/vscode-azext-utils"; -import { RegistryApi } from "./all/RegistryApi"; -import { IConnectRegistryWizardOptions } from "./connectWizard/IConnectRegistryWizardOptions"; -import { ICachedRegistryProvider } from "./ICachedRegistryProvider"; -import { IRegistryProviderTreeItem } from "./IRegistryProviderTreeItem"; - -export interface IRegistryProvider { - /** - * A unique id for this registry provider - */ - id: string; - - /** - * The api used by this provider - */ - api: RegistryApi; - - /** - * Primary value to display when prompting a user to connect a provider - */ - label: string; - - /** - * Optional secondary value to display when prompting a user to connect a provider - */ - description?: string; - - /** - * Optional tertiary value to display when prompting a user to connect a provider - */ - detail?: string; - - /** - * Set to true if this provider maps to a single registry as opposed to multiple registries - * If it maps to a single registry, it will be grouped under a "Connected Registries" node in the tree. - */ - isSingleRegistry?: boolean; - - /** - * Set to true if only a single instance of this provider can be connected at a time - */ - onlyOneAllowed?: boolean; - - /** - * Describes the wizard to be used when connecting this provider - */ - connectWizardOptions?: IConnectRegistryWizardOptions; - - /** - * The factory method for creating the root tree item - */ - treeItemFactory(parent: AzExtParentTreeItem, cachedProvider: ICachedRegistryProvider): (AzExtParentTreeItem & IRegistryProviderTreeItem) | Promise; - - /** - * Method to call for persisting auth secrets - */ - persistAuth?(cachedProvider: ICachedRegistryProvider, secret: string): Promise; - - /** - * Method to call to remove auth secrets - */ - removeAuth?(cachedProvider: ICachedRegistryProvider): Promise; -} diff --git a/src/tree/registries/IRegistryProviderTreeItem.ts b/src/tree/registries/IRegistryProviderTreeItem.ts deleted file mode 100644 index 5bac896097..0000000000 --- a/src/tree/registries/IRegistryProviderTreeItem.ts +++ /dev/null @@ -1,10 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ICachedRegistryProvider } from "./ICachedRegistryProvider"; - -export interface IRegistryProviderTreeItem { - cachedProvider: ICachedRegistryProvider; -} diff --git a/src/tree/registries/RegistriesTreeItem.ts b/src/tree/registries/RegistriesTreeItem.ts deleted file mode 100644 index b20ff0b8c4..0000000000 --- a/src/tree/registries/RegistriesTreeItem.ts +++ /dev/null @@ -1,233 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem, AzExtTreeItem, AzureWizard, GenericTreeItem, IActionContext, IAzureQuickPickItem, parseError, UserCancelledError } from "@microsoft/vscode-azext-utils"; -import { l10n, ThemeIcon } from "vscode"; -import { ext } from "../../extensionVariables"; -import { TreePrefix } from "../TreePrefix"; -import { getRegistryProviders } from "./all/getRegistryProviders"; -import { ConnectedRegistriesTreeItem } from "./ConnectedRegistriesTreeItem"; -import { IConnectRegistryWizardContext } from "./connectWizard/IConnectRegistryWizardContext"; -import { RegistryPasswordStep } from "./connectWizard/RegistryPasswordStep"; -import { RegistryUrlStep } from "./connectWizard/RegistryUrlStep"; -import { RegistryUsernameStep } from "./connectWizard/RegistryUsernameStep"; -import { ICachedRegistryProvider } from "./ICachedRegistryProvider"; -import { IRegistryProvider } from "./IRegistryProvider"; -import { IRegistryProviderTreeItem } from "./IRegistryProviderTreeItem"; -import { anyContextValuePart, contextValueSeparator } from "./registryContextValues"; -import { RegistryTreeItemBase } from "./RegistryTreeItemBase"; - -const providersKey = 'docker.registryProviders'; - -export class RegistriesTreeItem extends AzExtParentTreeItem { - public treePrefix: TreePrefix = 'registries'; - public static contextValue: string = 'registries'; - public contextValue: string = RegistriesTreeItem.contextValue; - public label: string = l10n.t('Registries'); - public childTypeLabel: string = 'registry provider'; - public autoSelectInTreeItemPicker: boolean = true; - - private _connectedRegistriesTreeItem: ConnectedRegistriesTreeItem; - private _cachedProviders: ICachedRegistryProvider[]; - - public constructor() { - super(undefined); - this._connectedRegistriesTreeItem = new ConnectedRegistriesTreeItem(this); - this._cachedProviders = ext.context.globalState.get(providersKey, []); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - if (this._cachedProviders.length === 0) { - return [new GenericTreeItem(this, { - label: l10n.t('Connect Registry...'), - contextValue: 'connectRegistry', - iconPath: new ThemeIcon('plug'), - includeInTreeItemPicker: true, - commandId: 'vscode-docker.registries.connectRegistry' - })]; - } else { - this._connectedRegistriesTreeItem.children = []; - const children: AzExtTreeItem[] = await this.createTreeItemsWithErrorHandling( - this._cachedProviders, - 'invalidRegistryProvider', - async cachedProvider => { - const provider = getRegistryProviders().find(rp => rp.id === cachedProvider.id); - if (!provider) { - throw new Error(l10n.t('Failed to find registry provider with id "{0}".', cachedProvider.id)); - } - - const parent = provider.isSingleRegistry ? this._connectedRegistriesTreeItem : this; - return this.initTreeItem(await Promise.resolve(provider.treeItemFactory(parent, cachedProvider))); - }, - cachedInfo => cachedInfo.id - ); - - this._connectedRegistriesTreeItem.children = children.filter(c => c.parent === this._connectedRegistriesTreeItem); - if (this._connectedRegistriesTreeItem.children.length > 0) { - children.push(this._connectedRegistriesTreeItem); - } - - return children.filter(c => c.parent !== this._connectedRegistriesTreeItem); - } - } - - public hasMoreChildrenImpl(): boolean { - return false; - } - - public async connectRegistry(context: IActionContext, provider?: IRegistryProvider, url?: string): Promise { - let picks: IAzureQuickPickItem[] = getRegistryProviders().map(rp => { - return { - label: rp.label, - description: rp.description, - detail: rp.detail, - data: rp - }; - }); - picks = picks.sort((p1, p2) => p1.label.localeCompare(p2.label)); - - const placeHolder: string = l10n.t('Select the provider for your registry'); - provider = provider ?? (await context.ui.showQuickPick(picks, { placeHolder, suppressPersistence: true })).data; - if (!provider) { - throw new UserCancelledError(); - } else if (provider.onlyOneAllowed && this._cachedProviders.find(c => c.id === provider.id)) { - // Don't wait, no input to wait for anyway - void context.ui.showWarningMessage(l10n.t('The "{0}" registry provider is already connected.', provider.label)); - throw new UserCancelledError('registryProviderAlreadyAdded'); - } - - context.telemetry.properties.providerId = provider.id; - context.telemetry.properties.providerApi = provider.api; - - const cachedProvider: ICachedRegistryProvider = { - id: provider.id, - api: provider.api, - }; - - if (provider.connectWizardOptions) { - const existingProviders: ICachedRegistryProvider[] = this._cachedProviders.filter(rp => rp.id === provider.id); - const wizardContext: IConnectRegistryWizardContext = { ...context, ...provider.connectWizardOptions, url, existingProviders }; - const wizard = new AzureWizard(wizardContext, { - title: provider.connectWizardOptions.wizardTitle, - promptSteps: [ - new RegistryUrlStep(), - new RegistryUsernameStep(), - new RegistryPasswordStep() - ] - }); - - await wizard.prompt(); - await wizard.execute(); - - cachedProvider.url = wizardContext.url; - cachedProvider.username = wizardContext.username; - - if (wizardContext.secret && provider.persistAuth) { - await provider.persistAuth(cachedProvider, wizardContext.secret); - } - } - - this._cachedProviders.push(cachedProvider); - await this.saveCachedProviders(context); - } - - public async disconnectRegistry(context: IActionContext, cachedProvider: ICachedRegistryProvider | undefined): Promise { - if (!cachedProvider) { - const picks = this._cachedProviders.map(crp => { - const provider = getRegistryProviders().find(rp => rp.id === crp.id); - const label: string = (provider && provider.label) || crp.id; - const descriptions: string[] = []; - if (crp.username) { - descriptions.push(l10n.t('Username: "{0}"', crp.username)); - } - if (crp.url) { - descriptions.push(l10n.t('URL: "{0}"', crp.url)); - } - return { - label, - description: descriptions[0], - detail: descriptions[1], - data: crp - }; - }); - const placeHolder: string = l10n.t('Select the registry to disconnect'); - cachedProvider = (await context.ui.showQuickPick(picks, { placeHolder, suppressPersistence: true })).data; - } - - context.telemetry.properties.providerId = cachedProvider.id; - context.telemetry.properties.providerApi = cachedProvider.api; - - // NOTE: Do not let failure prevent removal of the tree item. - - try { - const provider = getRegistryProviders().find(rp => rp.id === cachedProvider.id); - if (provider?.removeAuth) { - await provider.removeAuth(cachedProvider); - } - } catch (err) { - // Don't wait, no input to wait for anyway - void context.ui.showWarningMessage(l10n.t('The registry password could not be removed from the cache: {0}', parseError(err).message)); - } - - const index = this._cachedProviders.findIndex(n => n === cachedProvider); - if (index !== -1) { - this._cachedProviders.splice(index, 1); - } - - await this.saveCachedProviders(context); - } - - public hasMultiplesOfProvider(cachedProvider: ICachedRegistryProvider): boolean { - return this._cachedProviders.filter(c => c.id === cachedProvider.id).length > 1; - } - - public async getAllConnectedRegistries(context: IActionContext): Promise { - return await recursiveGetAllConnectedRegistries(context, ext.registriesRoot); - } - - private async saveCachedProviders(context: IActionContext): Promise { - await ext.context.globalState.update(providersKey, this._cachedProviders); - await this.refresh(context); - } - - private initTreeItem(node: AzExtParentTreeItem & IRegistryProviderTreeItem): AzExtParentTreeItem & IRegistryProviderTreeItem { - // Forcing all registry providers to have the same `isAncestorOfImpl` so that a provider doesn't show up for another provider's commands - node.isAncestorOfImpl = (expectedContextValue: string | RegExp) => { - expectedContextValue = expectedContextValue instanceof RegExp ? expectedContextValue.source.toString() : expectedContextValue; - - if (!expectedContextValue.includes(contextValueSeparator)) { - // If the expected context value has a non-standard format, just check against the id - // For example 'azureTask' is non-standard since it is unique to azure - return expectedContextValue.startsWith(node.cachedProvider.id); - } else { - const parts = expectedContextValue.split(contextValueSeparator); - if (parts[0] !== anyContextValuePart) { - return parts[0] === node.cachedProvider.id; - } else if (parts[1] !== anyContextValuePart) { - return parts[1] === node.cachedProvider.api; - } else { - // expectedContextValue must not have specificied any particular id or api, so return true - return true; - } - } - }; - - return node; - } -} - -async function recursiveGetAllConnectedRegistries(context: IActionContext, node: AzExtParentTreeItem): Promise { - let results: RegistryTreeItemBase[] = []; - - for (const child of await node.getCachedChildren(context)) { - if (child instanceof RegistryTreeItemBase) { - results.push(child); - } else if (child instanceof AzExtParentTreeItem) { - results = results.concat(await recursiveGetAllConnectedRegistries(context, child)); - } - } - - return results; -} diff --git a/src/tree/registries/RegistryConnectErrorTreeItem.ts b/src/tree/registries/RegistryConnectErrorTreeItem.ts deleted file mode 100644 index f67ef20a92..0000000000 --- a/src/tree/registries/RegistryConnectErrorTreeItem.ts +++ /dev/null @@ -1,24 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem, GenericTreeItem, parseError } from "@microsoft/vscode-azext-utils"; -import { ThemeColor, ThemeIcon } from "vscode"; -import { getRegistryProviders } from "./all/getRegistryProviders"; -import { ICachedRegistryProvider } from "./ICachedRegistryProvider"; -import { IRegistryProvider } from "./IRegistryProvider"; - -export class RegistryConnectErrorTreeItem extends GenericTreeItem { - public constructor(parent: AzExtParentTreeItem, err: unknown, public readonly cachedProvider: ICachedRegistryProvider, public readonly url?: string) { - super(parent, { - label: parseError(err).message, - contextValue: 'registryConnectError', - iconPath: new ThemeIcon('warning', new ThemeColor('problemsWarningIcon.foreground')), - }); - - this.provider = getRegistryProviders().find(rp => rp.id === this.cachedProvider.id); - } - - public readonly provider: IRegistryProvider; -} diff --git a/src/tree/registries/RegistryTreeItemBase.ts b/src/tree/registries/RegistryTreeItemBase.ts deleted file mode 100644 index 5e482e6ec5..0000000000 --- a/src/tree/registries/RegistryTreeItemBase.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem } from "@microsoft/vscode-azext-utils"; -import { ThemeIcon } from "vscode"; -import { RequestLike } from "../../utils/httpRequest"; -import { IRegistryAuthTreeItem } from "../../utils/registryRequestUtils"; -import { getRegistryContextValue, registrySuffix } from "./registryContextValues"; - -/** - * Base class for all registries - * NOTE: A registry is loosely defined as anything that contains repositories (e.g. a private registry or a Docker Hub namespace) - */ -export abstract class RegistryTreeItemBase extends AzExtParentTreeItem implements IRegistryAuthTreeItem { - public childTypeLabel: string = 'repository'; - - public constructor(parent: AzExtParentTreeItem | undefined) { - super(parent); - this.iconPath = new ThemeIcon('briefcase'); - } - - public get contextValue(): string { - return getRegistryContextValue(this, registrySuffix); - } - - /** - * Used for an image's full tag - * For example, if the full tag is "example.azurecr.io/hello-world:latest", this would return "example.azurecr.io" - * NOTE: This usually would _not_ include the protocol part of a url - */ - public abstract baseImagePath: string; - - /** - * Used for registry requests - * NOTE: This _should_ include the protocol part of a url - */ - public abstract baseUrl: string; - - /** - * This will be called before each registry request to add authentication - */ - public abstract signRequest(request: RequestLike): Promise; - - /** - * Describes credentials used to log in to the docker cli before pushing or pulling an image - */ - public abstract getDockerCliCredentials(): Promise; -} - -export interface IDockerCliCredentials { - /** - * Return either username/password or token credentials - * Return undefined if this registry doesn't require logging in to the docker cli - * NOTE: This may or may not be the same credentials used for registry requests - */ - auth?: { token: string } | { username: string; password: string }; - - /** - * The registry to log in to - * For central registries, this will usually be at an account level (aka empty for all of Docker Hub) - * For private registries, this will usually be at the registry level (aka the registry url) - */ - registryPath: string; -} diff --git a/src/tree/registries/RemoteRepositoryTreeItemBase.ts b/src/tree/registries/RemoteRepositoryTreeItemBase.ts deleted file mode 100644 index 1750bdf584..0000000000 --- a/src/tree/registries/RemoteRepositoryTreeItemBase.ts +++ /dev/null @@ -1,49 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem, AzExtTreeItem } from "@microsoft/vscode-azext-utils"; -import { ThemeIcon } from "vscode"; -import { RequestLike } from "../../utils/httpRequest"; -import { IRepositoryAuthTreeItem } from "../../utils/registryRequestUtils"; -import { getRegistryContextValue, repositorySuffix } from "./registryContextValues"; -import { RegistryTreeItemBase } from "./RegistryTreeItemBase"; -import { RemoteTagTreeItem } from "./RemoteTagTreeItem"; - -/** - * Base class for all repositories - */ -export abstract class RemoteRepositoryTreeItemBase extends AzExtParentTreeItem implements IRepositoryAuthTreeItem { - public childTypeLabel: string = 'tag'; - public parent: RegistryTreeItemBase; - public repoName: string; - - public constructor(parent: RegistryTreeItemBase, repoName: string) { - super(parent); - this.repoName = repoName; - this.iconPath = new ThemeIcon('repo'); - } - - public get label(): string { - return this.repoName; - } - - public get contextValue(): string { - return getRegistryContextValue(this, repositorySuffix); - } - - /** - * Optional method to implement if repo-level requests should have different authentication than registry-level requests - * For example, if the registry supports OAuth you might get a token that has just repo-level permissions instead of registry-level permissions - */ - public signRequest?(request: RequestLike): Promise; - - public compareChildrenImpl(ti1: AzExtTreeItem, ti2: AzExtTreeItem): number { - if (ti1 instanceof RemoteTagTreeItem && ti2 instanceof RemoteTagTreeItem) { - return ti2.time.valueOf() - ti1.time.valueOf(); - } else { - return super.compareChildrenImpl(ti1, ti2); - } - } -} diff --git a/src/tree/registries/RemoteTagTreeItem.ts b/src/tree/registries/RemoteTagTreeItem.ts deleted file mode 100644 index be6230349f..0000000000 --- a/src/tree/registries/RemoteTagTreeItem.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dayjs from 'dayjs'; -import * as relativeTime from 'dayjs/plugin/relativeTime'; -import { AzExtTreeItem } from '@microsoft/vscode-azext-utils'; -import { ThemeIcon } from 'vscode'; -import { getRegistryContextValue, tagSuffix } from './registryContextValues'; -import { RemoteRepositoryTreeItemBase } from './RemoteRepositoryTreeItemBase'; - -dayjs.extend(relativeTime); - -export class RemoteTagTreeItem extends AzExtTreeItem { - public parent: RemoteRepositoryTreeItemBase; - public tag: string; - public time: Date; - - public constructor(parent: RemoteRepositoryTreeItemBase, tag: string, time: string) { - super(parent); - this.tag = tag; - this.time = new Date(time); - } - - public get label(): string { - return this.tag; - } - - public get contextValue(): string { - return getRegistryContextValue(this, tagSuffix); - } - - /** - * The fullTag minus the registry part - */ - public get repoNameAndTag(): string { - return this.parent.repoName + ':' + this.tag; - } - - public get fullTag(): string { - return `${this.parent.parent.baseImagePath}/${this.repoNameAndTag}`; - } - - public get description(): string { - return dayjs(this.time).fromNow(); - } - - public get iconPath(): ThemeIcon { - return new ThemeIcon('bookmark'); - } -} diff --git a/src/tree/registries/UnifiedRegistryTreeDataProvider.ts b/src/tree/registries/UnifiedRegistryTreeDataProvider.ts new file mode 100644 index 0000000000..17e74fe6cb --- /dev/null +++ b/src/tree/registries/UnifiedRegistryTreeDataProvider.ts @@ -0,0 +1,220 @@ +import { CommonRegistry, CommonRegistryRoot, RegistryDataProvider, isRegistry } from '@microsoft/vscode-docker-registries'; +import * as vscode from 'vscode'; +import { ext } from '../../extensionVariables'; +import { isAzureSubscriptionRegistryItem } from './Azure/AzureRegistryDataProvider'; + +interface WrappedElement { + // eslint-disable-next-line @typescript-eslint/naming-convention + _urtdp_wrapper: UnifiedRegistryItem; +} + +function canReferenceWrapper(item: unknown): item is WrappedElement { + return !!item && typeof item === 'object'; +} + +export interface UnifiedRegistryItem { + provider: RegistryDataProvider; + wrappedItem: T; + parent: UnifiedRegistryItem | undefined; +} + +const ConnectedRegistryProvidersKey = 'ConnectedRegistryProviders'; + +export class UnifiedRegistryTreeDataProvider implements vscode.TreeDataProvider> { + private readonly onDidChangeTreeDataEmitter = new vscode.EventEmitter | UnifiedRegistryItem[] | undefined>(); + public readonly onDidChangeTreeData = this.onDidChangeTreeDataEmitter.event; + + private readonly providers = new Map>(); + + public constructor(private readonly storageMemento: vscode.Memento) { + + } + + public getTreeItem(element: UnifiedRegistryItem): vscode.TreeItem | Thenable { + return element.provider.getTreeItem(element.wrappedItem); + } + + public async getChildren(element?: UnifiedRegistryItem | undefined): Promise[]> { + if (element) { + const elements = await element.provider.getChildren(element.wrappedItem); + + if (!elements) { + return []; + } + + const results: UnifiedRegistryItem[] = []; + + for (const child of elements) { + const wrapper = { + provider: element.provider, + wrappedItem: child, + parent: element + }; + + if (canReferenceWrapper(child)) { + child._urtdp_wrapper = wrapper; + } + + results.push(wrapper); + } + + return results; + } else { + const unifiedRoots: UnifiedRegistryItem[] = []; + + const connectedProviderIds = this.storageMemento.get(ConnectedRegistryProvidersKey, []); + + for (const provider of this.providers.values()) { + if (!connectedProviderIds.includes(provider.id)) { + continue; + } + + const roots = await provider.getChildren(undefined); + if (!roots) { + continue; + } + + unifiedRoots.push(...roots.map(r => { + return { + provider, + wrappedItem: r, + parent: undefined + }; + })); + } + + return unifiedRoots; + } + } + + public getParent(element: UnifiedRegistryItem): UnifiedRegistryItem | undefined { + return element.parent; + } + + public registerProvider(provider: RegistryDataProvider): vscode.Disposable { + this.providers.set(provider.id, provider); + const disposable = provider.onDidChangeTreeData((child) => { + if (canReferenceWrapper(child) && child._urtdp_wrapper) { + this.onDidChangeTreeDataEmitter.fire(child._urtdp_wrapper); + } else { + this.onDidChangeTreeDataEmitter.fire(undefined); + } + }); + + return { + dispose: () => { + disposable.dispose(); + this.providers.delete(provider.id); + } + }; + } + + public async refresh(): Promise { + this.onDidChangeTreeDataEmitter.fire(undefined); + } + + public async connectRegistryProvider(provider: RegistryDataProvider | undefined = undefined): Promise { + const connectedProviderIds = this.storageMemento.get(ConnectedRegistryProvidersKey, []); + + if (!provider) { + const picks: (vscode.QuickPickItem & { provider: RegistryDataProvider })[] = []; + + for (const currentProvider of this.providers.values()) { + if (connectedProviderIds.includes(currentProvider.id)) { + continue; + } + + picks.push({ + label: currentProvider.label, + description: currentProvider.description, + provider: currentProvider + }); + } + + const picked = await vscode.window.showQuickPick(picks, { placeHolder: vscode.l10n.t('Select a registry provider to use') }); + if (!picked) { + return; + } + + provider = picked.provider; + } + + if (!connectedProviderIds.includes(provider.id)) { + await provider?.onConnect?.(); + const connectedProviderIdsSet: Set = new Set(connectedProviderIds); + connectedProviderIdsSet.add(provider.id); + await this.storageMemento.update(ConnectedRegistryProvidersKey, Array.from(connectedProviderIdsSet)); + } + + void this.refresh(); + } + + public async disconnectRegistryProvider(item: UnifiedRegistryItem): Promise { + await item.provider?.onDisconnect?.(); + + const newConnectedProviderIds = this.storageMemento + .get(ConnectedRegistryProvidersKey, []) + .filter(cpi => cpi !== item.provider.id); + await this.storageMemento.update(ConnectedRegistryProvidersKey, newConnectedProviderIds); + + void this.refresh(); + } + + /** + * + * @param imageBaseName The base name of the image to find registries for. e.g. 'docker.io' + * @returns A list of registries that are connected to the extension. If imageBaseName is provided, only registries that + * can be used to push to that image will be returned. + */ + public async getConnectedRegistries(imageBaseName?: string): Promise[]> { + let registryRoots = await this.getChildren(); + let findAzureRegistryOnly = false; + + // filter out registry roots that don't match the image base name + if (imageBaseName) { + if (imageBaseName === 'docker.io') { + registryRoots = registryRoots.filter(r => (r.wrappedItem as CommonRegistryRoot).label === ext.dockerHubRegistryDataProvider.label); + } + else if (imageBaseName.endsWith('azurecr.io')) { + registryRoots = registryRoots.filter(r => (r.wrappedItem as CommonRegistryRoot).label === ext.azureRegistryDataProvider.label); + findAzureRegistryOnly = true; + } + else if (imageBaseName === 'ghcr.io') { + registryRoots = registryRoots.filter(r => (r.wrappedItem as CommonRegistryRoot).label === ext.githubRegistryDataProvider.label); + } + else { + registryRoots = registryRoots.filter( + r => (r.wrappedItem as CommonRegistryRoot).label !== 'Docker Hub' + && (r.wrappedItem as CommonRegistryRoot).label !== 'Azure' + && (r.wrappedItem as CommonRegistryRoot).label !== 'GitHub'); + } + } + + const results: UnifiedRegistryItem[] = []; + + for (const registryRoot of registryRoots) { + try { + const maybeRegistries = await this.getChildren(registryRoot); + + for (const maybeRegistry of maybeRegistries) { + // short circuit if we're only looking for Azure registries + if (!findAzureRegistryOnly && isRegistry(maybeRegistry.wrappedItem)) { + results.push(maybeRegistry as UnifiedRegistryItem); + } else if (findAzureRegistryOnly || isAzureSubscriptionRegistryItem(maybeRegistry.wrappedItem)) { + const registries = await this.getChildren(maybeRegistry); + + for (const registry of registries) { + if (isRegistry(registry.wrappedItem)) { + results.push(registry as UnifiedRegistryItem); + } + } + } + } + } catch { + // best effort + } + } + + return results; + } +} diff --git a/src/tree/registries/all/RegistryApi.ts b/src/tree/registries/all/RegistryApi.ts deleted file mode 100644 index b52cb17815..0000000000 --- a/src/tree/registries/all/RegistryApi.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export enum RegistryApi { - /** - * https://docs.docker.com/registry/spec/api/ - */ - DockerV2 = 'DockerV2', - - /** - * https://docs.gitlab.com/ee/api/README.html - * https://docs.gitlab.com/ee/api/container_registry.html - */ - GitLabV4 = 'GitLabV4', - - /** - * No public docs found - */ - DockerHubV2 = 'DockerHubV2' -} diff --git a/src/tree/registries/all/getRegistryProviders.ts b/src/tree/registries/all/getRegistryProviders.ts deleted file mode 100644 index 5ad27a3133..0000000000 --- a/src/tree/registries/all/getRegistryProviders.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { azureRegistryProvider } from "../azure/azureRegistryProvider"; -import { dockerHubRegistryProvider } from "../dockerHub/dockerHubRegistryProvider"; -import { genericDockerV2RegistryProvider } from "../dockerV2/genericDockerV2RegistryProvider"; -import { gitLabRegistryProvider } from "../gitLab/gitLabRegistryProvider"; -import { IRegistryProvider } from "../IRegistryProvider"; - -export function getRegistryProviders(): IRegistryProvider[] { - return [ - azureRegistryProvider, - dockerHubRegistryProvider, - gitLabRegistryProvider, - genericDockerV2RegistryProvider - ]; -} diff --git a/src/tree/registries/auth/AzureOAuthProvider.ts b/src/tree/registries/auth/AzureOAuthProvider.ts deleted file mode 100644 index f65ead5c1b..0000000000 --- a/src/tree/registries/auth/AzureOAuthProvider.ts +++ /dev/null @@ -1,34 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ISubscriptionContext } from '@microsoft/vscode-azext-utils'; -import { acquireAcrAccessToken, acquireAcrRefreshToken } from '../../../utils/azureUtils'; -import { IOAuthContext, RequestLike, bearerAuthHeader } from '../../../utils/httpRequest'; -import { ICachedRegistryProvider } from '../ICachedRegistryProvider'; -import { IDockerCliCredentials } from '../RegistryTreeItemBase'; -import { IAuthProvider } from './IAuthProvider'; - -export interface IAzureOAuthContext extends IOAuthContext { - subscriptionContext: ISubscriptionContext -} - -class AzureOAuthProvider implements IAuthProvider { - - public async signRequest(cachedProvider: ICachedRegistryProvider, request: RequestLike, authContext: IAzureOAuthContext): Promise { - request.headers.set('Authorization', bearerAuthHeader(await acquireAcrAccessToken(authContext.realm.host, authContext.subscriptionContext, authContext.scope))); - return request; - } - - public async getDockerCliCredentials(cachedProvider: ICachedRegistryProvider, authContext?: IAzureOAuthContext): Promise { - return { - registryPath: `https://${authContext.service}`, - auth: { - token: await acquireAcrRefreshToken(authContext.realm.host, authContext.subscriptionContext), - }, - }; - } -} - -export const azureOAuthProvider: IAuthProvider = new AzureOAuthProvider(); diff --git a/src/tree/registries/auth/BasicOAuthProvider.ts b/src/tree/registries/auth/BasicOAuthProvider.ts deleted file mode 100644 index 73aaf5280b..0000000000 --- a/src/tree/registries/auth/BasicOAuthProvider.ts +++ /dev/null @@ -1,65 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { HttpResponse, IOAuthContext, RequestLike, RequestOptionsLike, basicAuthHeader, bearerAuthHeader, httpRequest } from '../../../utils/httpRequest'; -import { ICachedRegistryProvider } from '../ICachedRegistryProvider'; -import { getRegistryPassword } from '../registryPasswords'; -import { IDockerCliCredentials } from '../RegistryTreeItemBase'; -import { IAuthProvider } from './IAuthProvider'; - -/** - * Performs basic auth and password-grant-type OAuth - */ -class BasicOAuthProvider implements IAuthProvider { - - public async signRequest(cachedProvider: ICachedRegistryProvider, request: RequestLike, authContext?: IOAuthContext): Promise { - if (!authContext) { - request.headers.set('Authorization', basicAuthHeader(cachedProvider.username, await getRegistryPassword(cachedProvider))); - return request; - } - - const options: RequestOptionsLike = { - form: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'grant_type': 'password', - 'service': authContext.service, - 'scope': authContext.scope - }, - headers: { - Authorization: basicAuthHeader(cachedProvider.username, await getRegistryPassword(cachedProvider)), - }, - }; - - let tokenResponse: HttpResponse<{ token: string }>; - try { - // First try with POST - tokenResponse = await httpRequest(authContext.realm.toString(), { method: 'POST', ...options }); - } catch { - // If that fails, try falling back to GET - // (If that fails we'll just throw) - tokenResponse = await httpRequest(authContext.realm.toString(), { method: 'GET', ...options }); - } - - request.headers.set('Authorization', bearerAuthHeader((await tokenResponse.json()).token)); - return request; - } - - public async getDockerCliCredentials(cachedProvider: ICachedRegistryProvider, authContext?: IOAuthContext): Promise { - const creds: IDockerCliCredentials = { - registryPath: cachedProvider.url - }; - - if (cachedProvider.username) { - creds.auth = { - username: cachedProvider.username, - password: await getRegistryPassword(cachedProvider), - }; - } - - return creds; - } -} - -export const basicOAuthProvider: IAuthProvider = new BasicOAuthProvider(); diff --git a/src/tree/registries/auth/IAuthProvider.ts b/src/tree/registries/auth/IAuthProvider.ts deleted file mode 100644 index 6fa984c305..0000000000 --- a/src/tree/registries/auth/IAuthProvider.ts +++ /dev/null @@ -1,13 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IOAuthContext, RequestLike } from '../../../utils/httpRequest'; -import { ICachedRegistryProvider } from '../ICachedRegistryProvider'; -import { IDockerCliCredentials } from '../RegistryTreeItemBase'; - -export interface IAuthProvider { - signRequest(cachedProvider: ICachedRegistryProvider, request: RequestLike, authContext?: IOAuthContext): Promise; - getDockerCliCredentials(cachedProvider: ICachedRegistryProvider, authContext?: IOAuthContext): Promise; -} diff --git a/src/tree/registries/azure/AzureAccountTreeItem.ts b/src/tree/registries/azure/AzureAccountTreeItem.ts deleted file mode 100644 index 939b0f4d0d..0000000000 --- a/src/tree/registries/azure/AzureAccountTreeItem.ts +++ /dev/null @@ -1,40 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzureAccountTreeItemBase } from "@microsoft/vscode-azext-azureutils"; // This can't be made lazy, so users of this class must be lazy -import { AzExtParentTreeItem, AzExtTreeItem, IActionContext, ISubscriptionContext } from "@microsoft/vscode-azext-utils"; -import { Disposable } from "vscode"; -import { AzureAccountExtensionListener } from "../../../utils/AzureAccountExtensionListener"; -import { getAzSubTreeItem } from "../../../utils/lazyPackages"; -import { ICachedRegistryProvider } from "../ICachedRegistryProvider"; -import { IRegistryProviderTreeItem } from "../IRegistryProviderTreeItem"; -import { getRegistryContextValue, registryProviderSuffix } from "../registryContextValues"; -import type { SubscriptionTreeItem } from "./SubscriptionTreeItem"; - -export class AzureAccountTreeItem extends AzureAccountTreeItemBase implements IRegistryProviderTreeItem { - public constructor(parent: AzExtParentTreeItem, public readonly cachedProvider: ICachedRegistryProvider) { - super(parent); - this.contextValue = getRegistryContextValue(this, registryProviderSuffix); - } - - public async createSubscriptionTreeItem(subContext: ISubscriptionContext): Promise { - const azSubTreeItem = await getAzSubTreeItem(); - return new azSubTreeItem.SubscriptionTreeItem(this, subContext, this.cachedProvider); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - const treeItems: AzExtTreeItem[] = await super.loadMoreChildrenImpl(clearCache, context); - if (treeItems.length === 1 && treeItems[0].commandId === 'extension.open') { - const extensionInstallEventDisposable: Disposable = AzureAccountExtensionListener.onExtensionInstalled(() => { - extensionInstallEventDisposable.dispose(); - - // eslint-disable-next-line @typescript-eslint/no-floating-promises - this.refresh(context); - }); - } - - return treeItems; - } -} diff --git a/src/tree/registries/azure/AzureRegistryTreeItem.ts b/src/tree/registries/azure/AzureRegistryTreeItem.ts deleted file mode 100644 index d35fe97458..0000000000 --- a/src/tree/registries/azure/AzureRegistryTreeItem.ts +++ /dev/null @@ -1,115 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import type { ContainerRegistryManagementClient, Registry, RegistryListCredentialsResult } from "@azure/arm-containerregistry"; // These are only dev-time imports so don't need to be lazy -import { AzExtTreeItem, IActionContext, nonNullProp } from "@microsoft/vscode-azext-utils"; -import { URL } from "url"; -import { getResourceGroupFromId } from "../../../utils/azureUtils"; -import { getArmContainerRegistry, getAzExtAzureUtils } from "../../../utils/lazyPackages"; -import { getIconPath } from "../../getThemedIconPath"; -import { IAzureOAuthContext, azureOAuthProvider } from "../auth/AzureOAuthProvider"; -import { DockerV2RegistryTreeItemBase } from "../dockerV2/DockerV2RegistryTreeItemBase"; -import { ICachedRegistryProvider } from "../ICachedRegistryProvider"; -import { AzureRepositoryTreeItem } from "./AzureRepositoryTreeItem"; -import { AzureTasksTreeItem } from "./AzureTasksTreeItem"; -import type { SubscriptionTreeItem } from "./SubscriptionTreeItem"; // These are only dev-time imports so don't need to be lazy - -export class AzureRegistryTreeItem extends DockerV2RegistryTreeItemBase { - public parent: SubscriptionTreeItem; - - protected authContext?: IAzureOAuthContext; - - private _tasksTreeItem: AzureTasksTreeItem; - - public constructor(parent: SubscriptionTreeItem, cachedProvider: ICachedRegistryProvider, private readonly registry: Registry) { - super(parent, cachedProvider, azureOAuthProvider); - this._tasksTreeItem = new AzureTasksTreeItem(this); - this.authContext = { - realm: new URL(`${this.baseUrl}/oauth2/token`), - service: this.host, - subscriptionContext: this.parent.subscription, - scope: 'registry:catalog:*', - }; - - this.id = this.registryId; - this.iconPath = getIconPath('azureRegistry'); - } - - public get registryName(): string { - return nonNullProp(this.registry, 'name'); - } - - public get registryId(): string { - return nonNullProp(this.registry, 'id'); - } - - public get resourceGroup(): string { - return getResourceGroupFromId(this.registryId); - } - - public get registryLocation(): string { - return this.registry.location; - } - - public async getClient(context: IActionContext): Promise { - const azExtAzureUtils = await getAzExtAzureUtils(); - const armContainerRegistry = await getArmContainerRegistry(); - return azExtAzureUtils.createAzureClient({ ...context, ...this.subscription }, armContainerRegistry.ContainerRegistryManagementClient); - } - - public get label(): string { - return this.registryName; - } - - public get properties(): unknown { - return this.registry; - } - - public get baseUrl(): string { - return `https://${nonNullProp(this.registry, 'loginServer')}`; - } - - public createRepositoryTreeItem(name: string): AzureRepositoryTreeItem { - return new AzureRepositoryTreeItem(this, name, this.cachedProvider, this.authHelper, this.authContext); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - const children: AzExtTreeItem[] = await super.loadMoreChildrenImpl(clearCache, context); - if (clearCache) { - children.push(this._tasksTreeItem); - } - return children; - } - - public compareChildrenImpl(ti1: AzExtTreeItem, ti2: AzExtTreeItem): number { - if (ti1 instanceof AzureTasksTreeItem) { - return -1; - } else if (ti2 instanceof AzureTasksTreeItem) { - return 1; - } else { - return super.compareChildrenImpl(ti1, ti2); - } - } - - public async pickTreeItemImpl(expectedContextValues: (string | RegExp)[]): Promise { - if (expectedContextValues.some(v => this._tasksTreeItem.isAncestorOfImpl(v))) { - return this._tasksTreeItem; - } else { - return undefined; - } - } - - public async deleteTreeItemImpl(context: IActionContext): Promise { - await (await this.getClient(context)).registries.beginDeleteAndWait(this.resourceGroup, this.registryName); - } - - public async tryGetAdminCredentials(context: IActionContext): Promise { - if (this.registry.adminUserEnabled) { - return await (await this.getClient(context)).registries.listCredentials(this.resourceGroup, this.registryName); - } else { - return undefined; - } - } -} diff --git a/src/tree/registries/azure/AzureRepositoryTreeItem.ts b/src/tree/registries/azure/AzureRepositoryTreeItem.ts deleted file mode 100644 index 03893ffb78..0000000000 --- a/src/tree/registries/azure/AzureRepositoryTreeItem.ts +++ /dev/null @@ -1,19 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { registryRequest } from "../../../utils/registryRequestUtils"; -import { IAzureOAuthContext } from "../auth/AzureOAuthProvider"; -import { DockerV2RepositoryTreeItem } from "../dockerV2/DockerV2RepositoryTreeItem"; -import { AzureRegistryTreeItem } from "./AzureRegistryTreeItem"; - -export class AzureRepositoryTreeItem extends DockerV2RepositoryTreeItem { - public parent: AzureRegistryTreeItem; - - protected authContext?: IAzureOAuthContext; - - public async deleteTreeItemImpl(): Promise { - await registryRequest(this, 'DELETE', `v2/_acr/${this.repoName}/repository`); - } -} diff --git a/src/tree/registries/azure/AzureTaskRunTreeItem.ts b/src/tree/registries/azure/AzureTaskRunTreeItem.ts deleted file mode 100644 index c3d082d0eb..0000000000 --- a/src/tree/registries/azure/AzureTaskRunTreeItem.ts +++ /dev/null @@ -1,80 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dayjs from 'dayjs'; -import * as relativeTime from 'dayjs/plugin/relativeTime'; -import type { Run as AcrRun, ImageDescriptor } from "@azure/arm-containerregistry"; // These are only dev-time imports so don't need to be lazy -import { AzExtTreeItem, nonNullProp } from "@microsoft/vscode-azext-utils"; -import { ThemeColor, ThemeIcon } from "vscode"; -import { AzureTaskTreeItem } from "./AzureTaskTreeItem"; - -dayjs.extend(relativeTime); - -export class AzureTaskRunTreeItem extends AzExtTreeItem { - public static contextValue: string = 'azureTaskRun'; - public contextValue: string = AzureTaskRunTreeItem.contextValue; - public parent: AzureTaskTreeItem; - - private _run: AcrRun; - - public constructor(parent: AzureTaskTreeItem, run: AcrRun) { - super(parent); - this._run = run; - } - - public get runName(): string { - return nonNullProp(this._run, 'name'); - } - - public get runId(): string { - return nonNullProp(this._run, 'runId'); - } - - public get label(): string { - return this.runName; - } - - public get id(): string { - return this.runId; - } - - public get createTime(): Date | undefined { - return this._run.createTime; - } - - public get outputImage(): ImageDescriptor | undefined { - return this._run.outputImages && this._run.outputImages[0]; - } - - public get iconPath(): ThemeIcon { - switch (this._run.status) { - case 'Succeeded': - return new ThemeIcon('check', new ThemeColor('debugIcon.startForeground')); - case 'Failed': - return new ThemeIcon('error', new ThemeColor('problemsErrorIcon.foreground')); - case 'Running': - return new ThemeIcon('debug-start', new ThemeColor('debugIcon.startForeground')); - default: - return new ThemeIcon('warning', new ThemeColor('problemsWarningIcon.foreground')); - } - } - - public get properties(): unknown { - return this._run; - } - - public get description(): string { - const parts: string[] = []; - if (this.createTime) { - parts.push(dayjs(this.createTime).fromNow()); - } - - if (this._run.status && this._run.status !== 'Succeeded') { - parts.push(this._run.status); - } - - return parts.join(' - '); - } -} diff --git a/src/tree/registries/azure/AzureTaskTreeItem.ts b/src/tree/registries/azure/AzureTaskTreeItem.ts deleted file mode 100644 index 6dfa4aa369..0000000000 --- a/src/tree/registries/azure/AzureTaskTreeItem.ts +++ /dev/null @@ -1,90 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import type { Task as AcrTask, TaskRun as AcrTaskRun } from "@azure/arm-containerregistry"; // These are only dev-time imports so don't need to be lazy -import { AzExtParentTreeItem, AzExtTreeItem, GenericTreeItem, IActionContext, nonNullValue, nonNullValueAndProp } from "@microsoft/vscode-azext-utils"; -import { l10n, ThemeIcon } from "vscode"; -import { getAzExtAzureUtils } from "../../../utils/lazyPackages"; -import { AzureRegistryTreeItem } from "./AzureRegistryTreeItem"; -import { AzureTaskRunTreeItem } from "./AzureTaskRunTreeItem"; -import { AzureTasksTreeItem } from "./AzureTasksTreeItem"; - -export class AzureTaskTreeItem extends AzExtParentTreeItem { - public static contextValue: string = 'azureTask'; - private static _noTaskFilter: string = 'TaskName eq null'; - public childTypeLabel: string = 'task run'; - public parent: AzureTasksTreeItem; - - private _task: AcrTask | undefined; - - public constructor(parent: AzureTasksTreeItem, task: AcrTask | undefined) { - super(parent); - this._task = task; - this.iconPath = new ThemeIcon('tasklist'); - this.id = this._task ? this._task.id : undefined; - } - - public get contextValue(): string { - return this._task ? AzureTaskTreeItem.contextValue : 'azureRunsWithoutTask'; - } - - public get label(): string { - return this._task ? this.taskName : l10n.t('Runs without a task'); - } - - public get taskName(): string { - return nonNullValueAndProp(this._task, 'name'); - } - - public hasMoreChildrenImpl(): boolean { - return false; - } - - public get properties(): unknown { - return nonNullValue(this._task, '_task'); - } - - public static async hasRunsWithoutTask(context: IActionContext, registryTI: AzureRegistryTreeItem): Promise { - const runListResult = await AzureTaskTreeItem.getTaskRuns(context, registryTI, AzureTaskTreeItem._noTaskFilter); - return runListResult.length > 0; - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - const filter = this._task ? `TaskName eq '${this.taskName}'` : AzureTaskTreeItem._noTaskFilter; - const runListResult = await AzureTaskTreeItem.getTaskRuns(context, this.parent.parent, filter); - - if (clearCache && runListResult.length === 0 && this._task) { - const ti = new GenericTreeItem(this, { - label: l10n.t('Run Task...'), - commandId: 'vscode-docker.registries.azure.runTask', - contextValue: 'runTask' - }); - ti.commandArgs = [this]; - return [ti]; - } else { - return await this.createTreeItemsWithErrorHandling( - runListResult, - 'invalidAzureTaskRun', - async r => new AzureTaskRunTreeItem(this, r), - r => r.name - ); - } - } - - public compareChildrenImpl(ti1: AzExtTreeItem, ti2: AzExtTreeItem): number { - if (ti1 instanceof AzureTaskRunTreeItem && ti2 instanceof AzureTaskRunTreeItem && ti1.createTime && ti2.createTime) { - return ti2.createTime.valueOf() - ti1.createTime.valueOf(); - } else { - return super.compareChildrenImpl(ti1, ti2); - } - } - - private static async getTaskRuns(context: IActionContext, registryTI: AzureRegistryTreeItem, filter: string): Promise { - const azExtAzureUtils = await getAzExtAzureUtils(); - const registryClient = await registryTI.getClient(context); - - return await azExtAzureUtils.uiUtils.listAllIterator(registryClient.runs.list(registryTI.resourceGroup, registryTI.registryName, { filter })); - } -} diff --git a/src/tree/registries/azure/AzureTasksTreeItem.ts b/src/tree/registries/azure/AzureTasksTreeItem.ts deleted file mode 100644 index 82f3417de6..0000000000 --- a/src/tree/registries/azure/AzureTasksTreeItem.ts +++ /dev/null @@ -1,72 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import type { Task } from "@azure/arm-containerregistry"; // These are only dev-time imports so don't need to be lazy -import { AzExtParentTreeItem, AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; -import { l10n, ThemeIcon } from "vscode"; -import { getAzExtAzureUtils } from "../../../utils/lazyPackages"; -import { OpenUrlTreeItem } from "../../OpenUrlTreeItem"; -import { AzureRegistryTreeItem } from "./AzureRegistryTreeItem"; -import { AzureTaskTreeItem } from "./AzureTaskTreeItem"; - -export class AzureTasksTreeItem extends AzExtParentTreeItem { - public static contextValue: string = 'azureTasks'; - public contextValue: string = AzureTasksTreeItem.contextValue; - public label: string = 'Tasks'; - public childTypeLabel: string = 'task'; - public parent: AzureRegistryTreeItem; - - private _nextLink: string | undefined; - - public constructor(parent: AzureRegistryTreeItem) { - super(parent); - this.iconPath = new ThemeIcon('checklist'); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - if (clearCache) { - this._nextLink = undefined; - } - - const registryTI = this.parent; - - const azExtAzureUtils = await getAzExtAzureUtils(); - const registryClient = await registryTI.getClient(context); - - const taskListResult: Task[] = await azExtAzureUtils.uiUtils.listAllIterator(registryClient.tasks.list(registryTI.resourceGroup, registryTI.registryName)); - - if (clearCache && taskListResult.length === 0) { - return [new OpenUrlTreeItem(this, l10n.t('Learn how to create a build task...'), 'https://aka.ms/acr/task')]; - } else { - const result: AzExtTreeItem[] = await this.createTreeItemsWithErrorHandling( - taskListResult, - 'invalidAzureTask', - async t => new AzureTaskTreeItem(this, t), - t => t.name - ); - - if (clearCache) { - // If there are any runs _not_ associated with a task (e.g. the user ran a task from a local Dockerfile) add a tree item to display those runs - if (await AzureTaskTreeItem.hasRunsWithoutTask(context, this.parent)) { - result.push(new AzureTaskTreeItem(this, undefined)); - } - } - - return result; - } - } - - public hasMoreChildrenImpl(): boolean { - return !!this._nextLink; - } - - public isAncestorOfImpl(expectedContextValue: string | RegExp): boolean { - if (expectedContextValue instanceof RegExp) { - expectedContextValue = expectedContextValue.source.toString(); - } - - return expectedContextValue.toLowerCase().includes('task'); - } -} diff --git a/src/tree/registries/azure/SubscriptionTreeItem.ts b/src/tree/registries/azure/SubscriptionTreeItem.ts deleted file mode 100644 index 77769f7ba6..0000000000 --- a/src/tree/registries/azure/SubscriptionTreeItem.ts +++ /dev/null @@ -1,75 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import type { ContainerRegistryManagementClient, Registry as AcrRegistry } from '@azure/arm-containerregistry'; // These are only dev-time imports so don't need to be lazy -import { SubscriptionTreeItemBase } from '@microsoft/vscode-azext-azureutils'; // This can't be made lazy, so users of this class must be lazy -import { AzExtParentTreeItem, AzExtTreeItem, AzureWizard, IActionContext, ICreateChildImplContext, ISubscriptionContext, nonNullProp } from "@microsoft/vscode-azext-utils"; -import { l10n, window } from 'vscode'; -import { getArmContainerRegistry, getAzExtAzureUtils } from '../../../utils/lazyPackages'; -import { ICachedRegistryProvider } from "../ICachedRegistryProvider"; -import { IRegistryProviderTreeItem } from "../IRegistryProviderTreeItem"; -import type { AzureAccountTreeItem } from './AzureAccountTreeItem'; // These are only dev-time imports so don't need to be lazy -import { AzureRegistryTreeItem } from './AzureRegistryTreeItem'; -import { AzureRegistryCreateStep } from './createWizard/AzureRegistryCreateStep'; -import { AzureRegistryNameStep } from './createWizard/AzureRegistryNameStep'; -import { AzureRegistrySkuStep } from './createWizard/AzureRegistrySkuStep'; -import { IAzureRegistryWizardContext } from './createWizard/IAzureRegistryWizardContext'; - -export class SubscriptionTreeItem extends SubscriptionTreeItemBase implements IRegistryProviderTreeItem { - public childTypeLabel: string = 'registry'; - public parent: AzureAccountTreeItem; - - public constructor(parent: AzExtParentTreeItem, root: ISubscriptionContext, public readonly cachedProvider: ICachedRegistryProvider) { - super(parent, root); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - const armContainerRegistry = await getArmContainerRegistry(); - const azExtAzureUtils = await getAzExtAzureUtils(); - const client: ContainerRegistryManagementClient = azExtAzureUtils.createAzureClient({ ...context, ...this.subscription }, armContainerRegistry.ContainerRegistryManagementClient); - const registryListResult: AcrRegistry[] = await azExtAzureUtils.uiUtils.listAllIterator(client.registries.list()); - - return await this.createTreeItemsWithErrorHandling( - registryListResult, - 'invalidAzureRegistry', - async r => new AzureRegistryTreeItem(this, this.cachedProvider, r), - r => r.name - ); - } - - public hasMoreChildrenImpl(): boolean { - return false; - } - - public async createChildImpl(context: ICreateChildImplContext): Promise { - const wizardContext: IAzureRegistryWizardContext = { ...context, ...this.subscription }; - const azExtAzureUtils = await getAzExtAzureUtils(); - - const promptSteps = [ - new AzureRegistryNameStep(), - new AzureRegistrySkuStep(), - new azExtAzureUtils.ResourceGroupListStep(), - ]; - azExtAzureUtils.LocationListStep.addStep(wizardContext, promptSteps); - - const wizard = new AzureWizard(wizardContext, { - promptSteps, - executeSteps: [ - new AzureRegistryCreateStep() - ], - title: l10n.t('Create new Azure Container Registry') - }); - - await wizard.prompt(); - const newRegistryName: string = nonNullProp(wizardContext, 'newRegistryName'); - context.showCreatingTreeItem(newRegistryName); - await wizard.execute(); - - // don't wait - /* eslint-disable-next-line @typescript-eslint/no-floating-promises */ - window.showInformationMessage(`Successfully created registry "${newRegistryName}".`); - return new AzureRegistryTreeItem(this, this.cachedProvider, nonNullProp(wizardContext, 'registry')); - } -} diff --git a/src/tree/registries/azure/azureRegistryProvider.ts b/src/tree/registries/azure/azureRegistryProvider.ts deleted file mode 100644 index 3724f25503..0000000000 --- a/src/tree/registries/azure/azureRegistryProvider.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem } from "@microsoft/vscode-azext-utils"; -import { getAzActTreeItem } from "../../../utils/lazyPackages"; -import { RegistryApi } from "../all/RegistryApi"; -import { ICachedRegistryProvider } from "../ICachedRegistryProvider"; -import { IRegistryProvider } from "../IRegistryProvider"; -import type { AzureAccountTreeItem } from "./AzureAccountTreeItem"; // These are only dev-time imports so don't need to be lazy - -export const azureRegistryProviderId: string = 'azure'; - -export const azureRegistryProvider: IRegistryProvider = { - label: "Azure", - id: azureRegistryProviderId, - api: RegistryApi.DockerV2, - onlyOneAllowed: true, - connectWizardOptions: undefined, - treeItemFactory: async (parent: AzExtParentTreeItem, cachedProvider: ICachedRegistryProvider): Promise => { - const azActTreeItem = await getAzActTreeItem(); - return new azActTreeItem.AzureAccountTreeItem(parent, cachedProvider); - }, - persistAuth: undefined, - removeAuth: undefined, -}; diff --git a/src/tree/registries/connectWizard/IConnectRegistryWizardContext.ts b/src/tree/registries/connectWizard/IConnectRegistryWizardContext.ts deleted file mode 100644 index 360441afd8..0000000000 --- a/src/tree/registries/connectWizard/IConnectRegistryWizardContext.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IActionContext } from '@microsoft/vscode-azext-utils'; -import { ICachedRegistryProvider } from "../ICachedRegistryProvider"; -import { IConnectRegistryWizardOptions } from './IConnectRegistryWizardOptions'; - -export interface IConnectRegistryWizardContext extends IActionContext, IConnectRegistryWizardOptions { - existingProviders: ICachedRegistryProvider[]; - - username?: string; - secret?: string; - url?: string; -} diff --git a/src/tree/registries/connectWizard/IConnectRegistryWizardOptions.ts b/src/tree/registries/connectWizard/IConnectRegistryWizardOptions.ts deleted file mode 100644 index 2ca525ac0c..0000000000 --- a/src/tree/registries/connectWizard/IConnectRegistryWizardOptions.ts +++ /dev/null @@ -1,51 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export interface IConnectRegistryWizardOptions { - /** - * The title for the wizard (e.g. "Sign In To Docker Hub") - */ - wizardTitle: string; - - /** - * Set to true to prompt for a url - */ - includeUrl?: boolean; - - /** - * Optional value to overwrite the default text displayed underneath the url input box - */ - urlPrompt?: string; - - /** - * Set to true to prompt for a username - */ - includeUsername?: boolean; - - /** - * Optional value to overwrite the default prompt text displayed underneath the username input box - */ - usernamePrompt?: string; - - /** - * Optional value to overwrite the default "ghost" text displayed within the username input box - */ - usernamePlaceholder?: string; - - /** - * Set to true if the username is optional - */ - isUsernameOptional?: boolean; - - /** - * Set to true to prompt for a password - */ - includePassword?: boolean; - - /** - * Optional value to overwrite the default text displayed underneath the password input box - */ - passwordPrompt?: string; -} diff --git a/src/tree/registries/connectWizard/RegistryPasswordStep.ts b/src/tree/registries/connectWizard/RegistryPasswordStep.ts deleted file mode 100644 index a70e6e76ce..0000000000 --- a/src/tree/registries/connectWizard/RegistryPasswordStep.ts +++ /dev/null @@ -1,27 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; -import { l10n } from 'vscode'; -import { IConnectRegistryWizardContext } from './IConnectRegistryWizardContext'; - -export class RegistryPasswordStep extends AzureWizardPromptStep { - public async prompt(context: IConnectRegistryWizardContext): Promise { - const prompt: string = context.passwordPrompt || l10n.t('Enter your password'); - context.secret = await context.ui.showInputBox({ prompt, validateInput, password: true }); - } - - public shouldPrompt(context: IConnectRegistryWizardContext): boolean { - return !!context.includePassword && !context.secret; - } -} - -function validateInput(value: string | undefined): string | undefined { - if (!value) { - return l10n.t('Password cannot be empty.'); - } else { - return undefined; - } -} diff --git a/src/tree/registries/connectWizard/RegistryUrlStep.ts b/src/tree/registries/connectWizard/RegistryUrlStep.ts deleted file mode 100644 index b576bf70e3..0000000000 --- a/src/tree/registries/connectWizard/RegistryUrlStep.ts +++ /dev/null @@ -1,49 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; -import { URL } from 'url'; -import { l10n } from 'vscode'; -import { IConnectRegistryWizardContext } from './IConnectRegistryWizardContext'; - -export class RegistryUrlStep extends AzureWizardPromptStep { - public async prompt(context: IConnectRegistryWizardContext): Promise { - const prompt: string = context.urlPrompt || l10n.t('Enter the URL for the registry provider'); - const placeHolder: string = l10n.t('Example: http://localhost:5000'); - context.url = (await context.ui.showInputBox({ - prompt, - placeHolder, - validateInput: v => this.validateUrl(context, v) - })); - } - - public shouldPrompt(context: IConnectRegistryWizardContext): boolean { - return !!context.includeUrl && !context.url; - } - - private validateUrl(context: IConnectRegistryWizardContext, value: string): string | undefined { - if (!value) { - return l10n.t('URL cannot be empty.'); - } else { - let protocol: string | undefined; - let host: string | undefined; - try { - const uri = new URL(value); - protocol = uri.protocol; - host = uri.host; - } catch { - // ignore - } - - if (!protocol || !host) { - return l10n.t('Please enter a valid URL'); - } else if (context.existingProviders.find(rp => rp.url === value)) { - return l10n.t('URL "{0}" is already connected.', value); - } else { - return undefined; - } - } - } -} diff --git a/src/tree/registries/connectWizard/RegistryUsernameStep.ts b/src/tree/registries/connectWizard/RegistryUsernameStep.ts deleted file mode 100644 index 846db40e0d..0000000000 --- a/src/tree/registries/connectWizard/RegistryUsernameStep.ts +++ /dev/null @@ -1,39 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzureWizardPromptStep } from '@microsoft/vscode-azext-utils'; -import { InputBoxOptions, l10n } from 'vscode'; -import { IConnectRegistryWizardContext } from './IConnectRegistryWizardContext'; - -export class RegistryUsernameStep extends AzureWizardPromptStep { - public async prompt(context: IConnectRegistryWizardContext): Promise { - const prompt: string = context.usernamePrompt || (context.isUsernameOptional ? l10n.t('Enter your username, or press \'Enter\' for none') : l10n.t('Enter your username')); - const options: InputBoxOptions = { - prompt, - placeHolder: context.usernamePlaceholder, - validateInput: (value: string | undefined) => this.validateInput(context, value) - }; - - context.username = await context.ui.showInputBox(options); - - if (!context.username) { - context.includePassword = false; - } - } - - public shouldPrompt(context: IConnectRegistryWizardContext): boolean { - return !!context.includeUsername && !context.username; - } - - private validateInput(context: IConnectRegistryWizardContext, value: string | undefined): string | undefined { - if (!context.isUsernameOptional && !value) { - return l10n.t('Username cannot be empty.'); - } else if (context.existingProviders.find(rp => rp.url === context.url && rp.username === value)) { - return l10n.t('Username "{0}" is already connected.', value); - } else { - return undefined; - } - } -} diff --git a/src/tree/registries/dockerHub/DockerHubAccountTreeItem.ts b/src/tree/registries/dockerHub/DockerHubAccountTreeItem.ts deleted file mode 100644 index 663455ae19..0000000000 --- a/src/tree/registries/dockerHub/DockerHubAccountTreeItem.ts +++ /dev/null @@ -1,125 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem, AzExtTreeItem, IActionContext, nonNullProp } from "@microsoft/vscode-azext-utils"; -import { dockerHubUrl } from "../../../constants"; -import { ext } from "../../../extensionVariables"; -import { RequestLike, bearerAuthHeader } from "../../../utils/httpRequest"; -import { registryRequest } from "../../../utils/registryRequestUtils"; -import { getThemedIconPath } from "../../getThemedIconPath"; -import { ICachedRegistryProvider } from "../ICachedRegistryProvider"; -import { IRegistryProviderTreeItem } from "../IRegistryProviderTreeItem"; -import { RegistryConnectErrorTreeItem } from "../RegistryConnectErrorTreeItem"; -import { getRegistryContextValue, registryProviderSuffix } from "../registryContextValues"; -import { getRegistryPassword } from "../registryPasswords"; -import { DockerHubNamespaceTreeItem } from "./DockerHubNamespaceTreeItem"; - -export class DockerHubAccountTreeItem extends AzExtParentTreeItem implements IRegistryProviderTreeItem { - public label: string = 'Docker Hub'; - public childTypeLabel: string = 'namespace'; - public baseUrl: string = dockerHubUrl; - public cachedProvider: ICachedRegistryProvider; - - private _token?: string; - - public constructor(parent: AzExtParentTreeItem, cachedProvider: ICachedRegistryProvider) { - super(parent); - this.cachedProvider = cachedProvider; - this.id = this.cachedProvider.id + this.username; - this.iconPath = getThemedIconPath('docker'); - this.description = ext.registriesRoot.hasMultiplesOfProvider(this.cachedProvider) ? this.username : undefined; - } - - public get contextValue(): string { - return getRegistryContextValue(this, registryProviderSuffix); - } - - public get username(): string { - return nonNullProp(this.cachedProvider, 'username'); - } - - public async getPassword(): Promise { - return await getRegistryPassword(this.cachedProvider); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - if (clearCache) { - try { - await this.refreshToken(); - } catch (err) { - // If creds are invalid, the above refreshToken will fail - return [new RegistryConnectErrorTreeItem(this, err, this.cachedProvider)]; - } - } - - const orgsAndNamespaces = new Set(); - - for (const orgs of await this.getOrganizations()) { - orgsAndNamespaces.add(orgs); - } - - for (const namespaces of await this.getNamespaces()) { - orgsAndNamespaces.add(namespaces); - } - - return this.createTreeItemsWithErrorHandling( - Array.from(orgsAndNamespaces), - 'invalidDockerHubNamespace', - n => new DockerHubNamespaceTreeItem(this, n.toLowerCase()), - n => n - ); - } - - public hasMoreChildrenImpl(): boolean { - return false; - } - - public async signRequest(request: RequestLike): Promise { - if (this._token) { - request.headers.set('Authorization', bearerAuthHeader(this._token)); - } - - return request; - } - - private async refreshToken(): Promise { - this._token = undefined; - const url = 'v2/users/login'; - const body = { username: this.username, password: await this.getPassword() }; - // eslint-disable-next-line @typescript-eslint/naming-convention - const response = await registryRequest(this, 'POST', url, { body: JSON.stringify(body), headers: { 'Content-Type': 'application/json' } }); - this._token = response.body.token; - } - - private async getNamespaces(): Promise { - const url: string = `v2/repositories/namespaces`; - const response = await registryRequest(this, 'GET', url); - return response.body.namespaces; - } - - private async getOrganizations(): Promise { - const url: string = `v2/user/orgs`; - const response = await registryRequest(this, 'GET', url); - return response.body.results?.map(o => o.orgname) ?? []; - } -} - -interface IToken { - token: string -} - -interface INamespaces { - namespaces: string[]; - next?: string; -} - -interface IOrganizations { - results: [ - { - orgname: string - } - ], - next?: string; -} diff --git a/src/tree/registries/dockerHub/DockerHubNamespaceTreeItem.ts b/src/tree/registries/dockerHub/DockerHubNamespaceTreeItem.ts deleted file mode 100644 index a28f2c45c8..0000000000 --- a/src/tree/registries/dockerHub/DockerHubNamespaceTreeItem.ts +++ /dev/null @@ -1,76 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; -import { PAGE_SIZE, dockerHubUrl } from "../../../constants"; -import { RequestLike } from "../../../utils/httpRequest"; -import { registryRequest } from "../../../utils/registryRequestUtils"; -import { IDockerCliCredentials, RegistryTreeItemBase } from "../RegistryTreeItemBase"; -import { DockerHubAccountTreeItem } from "./DockerHubAccountTreeItem"; -import { DockerHubRepositoryTreeItem } from "./DockerHubRepositoryTreeItem"; - -export class DockerHubNamespaceTreeItem extends RegistryTreeItemBase { - public parent: DockerHubAccountTreeItem; - public baseUrl: string = dockerHubUrl; - public namespace: string; - - private _nextLink: string | undefined; - - public constructor(parent: DockerHubAccountTreeItem, namespace: string) { - super(parent); - this.namespace = namespace; - } - - public get label(): string { - return this.namespace; - } - - public get baseImagePath(): string { - return this.namespace; - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - if (clearCache) { - this._nextLink = undefined; - } - - const url = this._nextLink || `v2/repositories/${this.namespace}?page_size=${PAGE_SIZE}`; - const response = await registryRequest(this, 'GET', url); - this._nextLink = response.body.next; - return await this.createTreeItemsWithErrorHandling( - response.body.results, - 'invalidRepository', - r => new DockerHubRepositoryTreeItem(this, r.name), - r => r.name - ); - } - - public hasMoreChildrenImpl(): boolean { - return !!this._nextLink; - } - - public async signRequest(request: RequestLike): Promise { - return this.parent.signRequest(request); - } - - public async getDockerCliCredentials(): Promise { - return { - registryPath: '', - auth: { - username: this.parent.username, - password: await this.parent.getPassword() - } - }; - } -} - -interface IRepositories { - results: IRepository[]; - next?: string; -} - -interface IRepository { - name: string; -} diff --git a/src/tree/registries/dockerHub/DockerHubRepositoryTreeItem.ts b/src/tree/registries/dockerHub/DockerHubRepositoryTreeItem.ts deleted file mode 100644 index 3040ac7287..0000000000 --- a/src/tree/registries/dockerHub/DockerHubRepositoryTreeItem.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; -import { PAGE_SIZE } from "../../../constants"; -import { registryRequest } from "../../../utils/registryRequestUtils"; -import { RemoteRepositoryTreeItemBase } from "../RemoteRepositoryTreeItemBase"; -import { RemoteTagTreeItem } from "../RemoteTagTreeItem"; -import { DockerHubNamespaceTreeItem } from "./DockerHubNamespaceTreeItem"; - -export class DockerHubRepositoryTreeItem extends RemoteRepositoryTreeItemBase { - public parent: DockerHubNamespaceTreeItem; - - private _nextLink: string | undefined; - - public constructor(parent: DockerHubNamespaceTreeItem, name: string) { - super(parent, name); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - if (clearCache) { - this._nextLink = undefined; - } - - const url = this._nextLink || `v2/repositories/${this.parent.namespace}/${this.repoName}/tags?page_size=${PAGE_SIZE}`; - const response = await registryRequest(this, 'GET', url); - this._nextLink = response.body.next; - return await this.createTreeItemsWithErrorHandling( - response.body.results, - 'invalidTag', - async t => new RemoteTagTreeItem(this, t.name, t.last_updated), - t => t.name - ); - } - - public hasMoreChildrenImpl(): boolean { - return !!this._nextLink; - } -} - -interface ITags { - next?: string; - results: ITag[]; -} - -interface ITag { - name: string; - // eslint-disable-next-line @typescript-eslint/naming-convention - last_updated: string; -} diff --git a/src/tree/registries/dockerHub/dockerHubRegistryProvider.ts b/src/tree/registries/dockerHub/dockerHubRegistryProvider.ts deleted file mode 100644 index 15cf22dcb0..0000000000 --- a/src/tree/registries/dockerHub/dockerHubRegistryProvider.ts +++ /dev/null @@ -1,29 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { l10n } from 'vscode'; -import { RegistryApi } from "../all/RegistryApi"; -import { IRegistryProvider } from "../IRegistryProvider"; -import { deleteRegistryPassword, setRegistryPassword } from '../registryPasswords'; -import { DockerHubAccountTreeItem } from "./DockerHubAccountTreeItem"; - -export const dockerHubRegistryProviderId: string = 'dockerHub'; - -export const dockerHubRegistryProvider: IRegistryProvider = { - label: "Docker Hub", - id: dockerHubRegistryProviderId, - api: RegistryApi.DockerHubV2, - connectWizardOptions: { - wizardTitle: l10n.t('Sign in to Docker Hub'), - includeUsername: true, - usernamePrompt: l10n.t('Visit hub.docker.com to sign up for a Docker ID'), - usernamePlaceholder: l10n.t('Enter your Docker ID'), - passwordPrompt: l10n.t('Enter your password or personal access token'), - includePassword: true, - }, - treeItemFactory: (parent, cachedProvider) => new DockerHubAccountTreeItem(parent, cachedProvider), - persistAuth: async (cachedProvider, secret) => await setRegistryPassword(cachedProvider, secret), - removeAuth: async (cachedProvider) => await deleteRegistryPassword(cachedProvider), -}; diff --git a/src/tree/registries/dockerV2/DockerV2RegistryTreeItemBase.ts b/src/tree/registries/dockerV2/DockerV2RegistryTreeItemBase.ts deleted file mode 100644 index 0266d53c99..0000000000 --- a/src/tree/registries/dockerV2/DockerV2RegistryTreeItemBase.ts +++ /dev/null @@ -1,75 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem, AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; -import { URL } from "url"; -import { PAGE_SIZE } from "../../../constants"; -import { IOAuthContext, RequestLike } from "../../../utils/httpRequest"; -import { getNextLinkFromHeaders, registryRequest } from "../../../utils/registryRequestUtils"; -import { IAuthProvider } from "../auth/IAuthProvider"; -import { ICachedRegistryProvider } from "../ICachedRegistryProvider"; -import { IRegistryProviderTreeItem } from "../IRegistryProviderTreeItem"; -import { IDockerCliCredentials, RegistryTreeItemBase } from "../RegistryTreeItemBase"; -import { RemoteRepositoryTreeItemBase } from "../RemoteRepositoryTreeItemBase"; - -export abstract class DockerV2RegistryTreeItemBase extends RegistryTreeItemBase implements IRegistryProviderTreeItem { - protected authContext?: IOAuthContext; - - private _nextLink: string | undefined; - - public constructor(parent: AzExtParentTreeItem, public readonly cachedProvider: ICachedRegistryProvider, protected readonly authHelper: IAuthProvider) { - super(parent); - } - - public get baseImagePath(): string { - return this.host.toLowerCase(); - } - - public get host(): string { - return new URL(this.baseUrl).host; - } - - public abstract createRepositoryTreeItem(name: string): RemoteRepositoryTreeItemBase; - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - if (clearCache) { - this._nextLink = undefined; - } - - const url = this._nextLink || `v2/_catalog?n=${PAGE_SIZE}`; - const response = await registryRequest(this, 'GET', url); - this._nextLink = getNextLinkFromHeaders(response); - return await this.createTreeItemsWithErrorHandling( - response.body.repositories, - 'invalidRepository', - r => this.createRepositoryTreeItem(r), - r => r - ); - } - - public hasMoreChildrenImpl(): boolean { - return !!this._nextLink; - } - - public async signRequest(request: RequestLike): Promise { - if (this.authHelper) { - return this.authHelper.signRequest(this.cachedProvider, request, this.authContext); - } - - return request; - } - - public async getDockerCliCredentials(): Promise { - if (this.authHelper) { - return await this.authHelper.getDockerCliCredentials(this.cachedProvider, this.authContext); - } - - return undefined; - } -} - -interface IRepositories { - repositories: string[]; -} diff --git a/src/tree/registries/dockerV2/DockerV2RepositoryTreeItem.ts b/src/tree/registries/dockerV2/DockerV2RepositoryTreeItem.ts deleted file mode 100644 index 63dd0448a6..0000000000 --- a/src/tree/registries/dockerV2/DockerV2RepositoryTreeItem.ts +++ /dev/null @@ -1,89 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; -import { PAGE_SIZE } from "../../../constants"; -import { ErrorHandling, HttpErrorResponse, HttpStatusCode, IOAuthContext, RequestLike } from "../../../utils/httpRequest"; -import { getNextLinkFromHeaders, registryRequest } from "../../../utils/registryRequestUtils"; -import { IAuthProvider } from "../auth/IAuthProvider"; -import { ICachedRegistryProvider } from "../ICachedRegistryProvider"; -import { IRegistryProviderTreeItem } from "../IRegistryProviderTreeItem"; -import { RemoteRepositoryTreeItemBase } from "../RemoteRepositoryTreeItemBase"; -import { DockerV2RegistryTreeItemBase } from "./DockerV2RegistryTreeItemBase"; -import { DockerV2TagTreeItem } from "./DockerV2TagTreeItem"; - -export class DockerV2RepositoryTreeItem extends RemoteRepositoryTreeItemBase implements IRegistryProviderTreeItem { - public parent: DockerV2RegistryTreeItemBase; - - private _nextLink: string | undefined; - - public constructor(parent: DockerV2RegistryTreeItemBase, repoName: string, public readonly cachedProvider: ICachedRegistryProvider, protected readonly authHelper: IAuthProvider, protected readonly authContext?: IOAuthContext) { - super(parent, repoName); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - if (clearCache) { - this._nextLink = undefined; - } - - const url = this._nextLink || `v2/${this.repoName}/tags/list?n=${PAGE_SIZE}`; - const response = await registryRequest(this, 'GET', url, undefined, ErrorHandling.ReturnErrorResponse); - if (response.status === HttpStatusCode.NotFound) { - // Some registries return 404 when all tags have been removed and the repository becomes effectively unavailable. - void this.deleteTreeItem(context); - return []; - } - else if (!response.ok) { - throw new HttpErrorResponse(response); - } - - this._nextLink = getNextLinkFromHeaders(response); - return await this.createTreeItemsWithErrorHandling( - response.body.tags, - 'invalidTag', - async t => { - const time = await this.getTagTime(t); - return new DockerV2TagTreeItem(this, t, time); - }, - t => t - ); - } - - public async signRequest(request: RequestLike): Promise { - if (this.authHelper) { - const authContext: IOAuthContext | undefined = this.authContext ? { ...this.authContext, scope: `repository:${this.repoName}:${request.method === 'DELETE' ? '*' : 'pull'}` } : undefined; - return this.authHelper.signRequest(this.cachedProvider, request, authContext); - } - - return request; - } - - public hasMoreChildrenImpl(): boolean { - return !!this._nextLink; - } - - private async getTagTime(tag: string): Promise { - const manifestUrl: string = `v2/${this.repoName}/manifests/${tag}`; - const manifestResponse = await registryRequest(this, 'GET', manifestUrl); - const history = JSON.parse(manifestResponse.body.history[0].v1Compatibility); - return history.created; - } -} - -interface ITags { - tags: string[]; -} - -interface IManifestHistory { - v1Compatibility: string; // stringified ManifestHistoryV1Compatibility -} - -interface IManifestHistoryV1Compatibility { - created: string; -} - -interface IManifest { - history: IManifestHistory[]; -} diff --git a/src/tree/registries/dockerV2/DockerV2TagTreeItem.ts b/src/tree/registries/dockerV2/DockerV2TagTreeItem.ts deleted file mode 100644 index 1ee3516e62..0000000000 --- a/src/tree/registries/dockerV2/DockerV2TagTreeItem.ts +++ /dev/null @@ -1,44 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IActionContext, UserCancelledError, parseError } from '@microsoft/vscode-azext-utils'; -import { registryRequest } from '../../../utils/registryRequestUtils'; -import { RemoteTagTreeItem } from '../RemoteTagTreeItem'; - -export class DockerV2TagTreeItem extends RemoteTagTreeItem { - public async getDigest(): Promise { - const digestOptions = { - headers: { - // According to https://docs.docker.com/registry/spec/api/ - // When deleting a manifest from a registry version 2.3 or later, the following header must be used when HEAD or GET-ing the manifest to obtain the correct digest to delete - accept: 'application/vnd.docker.distribution.manifest.v2+json' - } - }; - - const url = `v2/${this.parent.repoName}/manifests/${this.tag}`; - const response = await registryRequest(this.parent, 'GET', url, digestOptions); - const digest = response.headers.get('docker-content-digest') as string; - return digest; - } - - public async deleteTreeItemImpl(context: IActionContext): Promise { - const digest = await this.getDigest(); - const url = `v2/${this.parent.repoName}/manifests/${digest}`; - - try { - await registryRequest(this.parent, 'DELETE', url); - } catch (error) { - const errorType: string = parseError(error).errorType.toLowerCase(); - if (errorType === '405' || errorType === 'unsupported') { - // Don't wait - // eslint-disable-next-line @typescript-eslint/no-floating-promises - context.ui.showWarningMessage('Deleting remote images is not supported on this registry. It may need to be enabled.', { learnMoreLink: 'https://aka.ms/AA7jsql' }); - throw new UserCancelledError(); - } else { - throw error; - } - } - } -} diff --git a/src/tree/registries/dockerV2/GenericDockerV2RegistryTreeItem.ts b/src/tree/registries/dockerV2/GenericDockerV2RegistryTreeItem.ts deleted file mode 100644 index 9b6336ee37..0000000000 --- a/src/tree/registries/dockerV2/GenericDockerV2RegistryTreeItem.ts +++ /dev/null @@ -1,56 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem, AzExtTreeItem, IActionContext, nonNullProp } from "@microsoft/vscode-azext-utils"; -import { HttpErrorResponse, getWwwAuthenticateContext } from "../../../utils/httpRequest"; -import { registryRequest } from "../../../utils/registryRequestUtils"; -import { IAuthProvider } from "../auth/IAuthProvider"; -import { ICachedRegistryProvider } from "../ICachedRegistryProvider"; -import { getRegistryContextValue, registryProviderSuffix, registrySuffix } from "../registryContextValues"; -import { DockerV2RegistryTreeItemBase } from "./DockerV2RegistryTreeItemBase"; -import { DockerV2RepositoryTreeItem } from "./DockerV2RepositoryTreeItem"; - -export class GenericDockerV2RegistryTreeItem extends DockerV2RegistryTreeItemBase { - public constructor(parent: AzExtParentTreeItem, cachedProvider: ICachedRegistryProvider, authHelper: IAuthProvider) { - super(parent, cachedProvider, authHelper); - this.id = this.baseUrl; - } - - public get contextValue(): string { - return getRegistryContextValue(this, registrySuffix, registryProviderSuffix); - } - - public get label(): string { - return this.host; - } - - public get baseUrl(): string { - return nonNullProp(this.cachedProvider, 'url'); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - if (clearCache) { - try { - // If the call succeeds, it's a V2 registry (https://docs.docker.com/registry/spec/api/#api-version-check) - // NOTE: Trailing slash is necessary (https://github.com/microsoft/vscode-docker/issues/1142) - await registryRequest(this, 'GET', 'v2/'); - } catch (error) { - if (error instanceof HttpErrorResponse && - (this.authContext = getWwwAuthenticateContext(error))) { - // We got authentication context successfully--set scope and move on to requesting the items - this.authContext.scope = 'registry:catalog:*'; - } else { - throw error; - } - } - } - - return super.loadMoreChildrenImpl(clearCache, context); - } - - public createRepositoryTreeItem(name: string): DockerV2RepositoryTreeItem { - return new DockerV2RepositoryTreeItem(this, name, this.cachedProvider, this.authHelper, this.authContext); - } -} diff --git a/src/tree/registries/dockerV2/genericDockerV2RegistryProvider.ts b/src/tree/registries/dockerV2/genericDockerV2RegistryProvider.ts deleted file mode 100644 index 9054b212c7..0000000000 --- a/src/tree/registries/dockerV2/genericDockerV2RegistryProvider.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { l10n } from 'vscode'; -import { RegistryApi } from "../all/RegistryApi"; -import { basicOAuthProvider } from '../auth/BasicOAuthProvider'; -import { IRegistryProvider } from "../IRegistryProvider"; -import { deleteRegistryPassword, setRegistryPassword } from '../registryPasswords'; -import { GenericDockerV2RegistryTreeItem } from "./GenericDockerV2RegistryTreeItem"; - -export const genericDockerV2RegistryProvider: IRegistryProvider = { - label: l10n.t('Generic Docker Registry'), - description: l10n.t('(Preview)'), - detail: l10n.t('Connect any generic private registry that supports the "Docker V2" api.'), - id: 'genericDockerV2', - api: RegistryApi.DockerV2, - isSingleRegistry: true, - connectWizardOptions: { - wizardTitle: l10n.t('Connect Docker Registry'), - includeUrl: true, - urlPrompt: l10n.t('Enter the URL for the registry'), - includeUsername: true, - isUsernameOptional: true, - includePassword: true, - }, - treeItemFactory: (parent, cachedProvider) => new GenericDockerV2RegistryTreeItem(parent, cachedProvider, basicOAuthProvider), - persistAuth: async (cachedProvider, secret) => await setRegistryPassword(cachedProvider, secret), - removeAuth: async (cachedProvider) => await deleteRegistryPassword(cachedProvider), -}; diff --git a/src/tree/registries/gitLab/GitLabAccountTreeItem.ts b/src/tree/registries/gitLab/GitLabAccountTreeItem.ts deleted file mode 100644 index 4db3a4a873..0000000000 --- a/src/tree/registries/gitLab/GitLabAccountTreeItem.ts +++ /dev/null @@ -1,86 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtParentTreeItem, AzExtTreeItem, IActionContext, nonNullProp, parseError } from "@microsoft/vscode-azext-utils"; -import { PAGE_SIZE } from "../../../constants"; -import { ext } from "../../../extensionVariables"; -import { RequestLike } from "../../../utils/httpRequest"; -import { getNextLinkFromHeaders, registryRequest } from "../../../utils/registryRequestUtils"; -import { getIconPath } from "../../getThemedIconPath"; -import { ICachedRegistryProvider } from "../ICachedRegistryProvider"; -import { IRegistryProviderTreeItem } from "../IRegistryProviderTreeItem"; -import { RegistryConnectErrorTreeItem } from "../RegistryConnectErrorTreeItem"; -import { getRegistryContextValue, registryProviderSuffix } from "../registryContextValues"; -import { getRegistryPassword } from "../registryPasswords"; -import { GitLabProjectTreeItem } from "./GitLabProjectTreeItem"; - -export class GitLabAccountTreeItem extends AzExtParentTreeItem implements IRegistryProviderTreeItem { - public label: string = 'GitLab'; - public childTypeLabel: string = 'project'; - public baseUrl: string = 'https://gitlab.com/'; - public cachedProvider: ICachedRegistryProvider; - - private _nextLink?: string; - - public constructor(parent: AzExtParentTreeItem, provider: ICachedRegistryProvider) { - super(parent); - this.cachedProvider = provider; - this.id = this.cachedProvider.id + this.username; - this.iconPath = getIconPath('gitlab'); - this.description = ext.registriesRoot.hasMultiplesOfProvider(this.cachedProvider) ? this.username : undefined; - } - - public get contextValue(): string { - return getRegistryContextValue(this, registryProviderSuffix); - } - - public get username(): string { - return nonNullProp(this.cachedProvider, 'username'); - } - - public async getPassword(): Promise { - return await getRegistryPassword(this.cachedProvider); - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - if (clearCache) { - this._nextLink = undefined; - } - - try { - const url: string = this._nextLink || `api/v4/projects?per_page=${PAGE_SIZE}&simple=true&membership=true`; - const response = await registryRequest(this, 'GET', url); - this._nextLink = getNextLinkFromHeaders(response); - return this.createTreeItemsWithErrorHandling( - response.body, - 'invalidGitLabProject', - n => new GitLabProjectTreeItem(this, n.id.toString(), n.path_with_namespace.toLowerCase()), - n => n.path_with_namespace - ); - } catch (err) { - const errorType: string = parseError(err).errorType.toLowerCase(); - if (errorType === '401' || errorType === 'unauthorized') { - return [new RegistryConnectErrorTreeItem(this, err, this.cachedProvider)]; - } else { - throw err; - } - } - } - - public hasMoreChildrenImpl(): boolean { - return !!this._nextLink; - } - - public async signRequest(request: RequestLike): Promise { - request.headers.set('PRIVATE-TOKEN', await this.getPassword()); - return request; - } -} - -interface IProject { - id: number; - // eslint-disable-next-line @typescript-eslint/naming-convention - path_with_namespace: string; -} diff --git a/src/tree/registries/gitLab/GitLabProjectTreeItem.ts b/src/tree/registries/gitLab/GitLabProjectTreeItem.ts deleted file mode 100644 index dfb917fe54..0000000000 --- a/src/tree/registries/gitLab/GitLabProjectTreeItem.ts +++ /dev/null @@ -1,80 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; -import { PAGE_SIZE } from "../../../constants"; -import { RequestLike } from "../../../utils/httpRequest"; -import { getNextLinkFromHeaders, registryRequest } from "../../../utils/registryRequestUtils"; -import { IDockerCliCredentials, RegistryTreeItemBase } from "../RegistryTreeItemBase"; -import { GitLabAccountTreeItem } from "./GitLabAccountTreeItem"; -import { GitLabRepositoryTreeItem } from "./GitLabRepositoryTreeItem"; - -const gitLabRegistryUrl: string = 'registry.gitlab.com'; - -export class GitLabProjectTreeItem extends RegistryTreeItemBase { - public parent: GitLabAccountTreeItem; - public readonly projectId: string; - public pathWithNamespace: string; - - private _nextLink?: string; - - public constructor(parent: GitLabAccountTreeItem, id: string, pathWithNamespace: string) { - super(parent); - this.projectId = id; - this.pathWithNamespace = pathWithNamespace; - this.id = this.projectId; - } - - public get baseUrl(): string { - return this.parent.baseUrl; - } - - public get label(): string { - return this.pathWithNamespace; - } - - public get baseImagePath(): string { - return gitLabRegistryUrl + '/' + this.pathWithNamespace; - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - if (clearCache) { - this._nextLink = undefined; - } - - const url = this._nextLink || `api/v4/projects/${this.projectId}/registry/repositories?per_page=${PAGE_SIZE}`; - const response = await registryRequest(this, 'GET', url); - this._nextLink = getNextLinkFromHeaders(response); - return await this.createTreeItemsWithErrorHandling( - response.body, - 'invalidRepository', - r => new GitLabRepositoryTreeItem(this, r.id.toString(), r.name), - r => r.name - ); - } - - public hasMoreChildrenImpl(): boolean { - return !!this._nextLink; - } - - public async signRequest(request: RequestLike): Promise { - return this.parent.signRequest(request); - } - - public async getDockerCliCredentials(): Promise { - return { - registryPath: gitLabRegistryUrl, - auth: { - username: this.parent.username, - password: await this.parent.getPassword() - } - }; - } -} - -interface IRepository { - name: string; - id: number; -} diff --git a/src/tree/registries/gitLab/GitLabRepositoryTreeItem.ts b/src/tree/registries/gitLab/GitLabRepositoryTreeItem.ts deleted file mode 100644 index b0fe1ced00..0000000000 --- a/src/tree/registries/gitLab/GitLabRepositoryTreeItem.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtTreeItem, IActionContext } from "@microsoft/vscode-azext-utils"; -import { PAGE_SIZE } from "../../../constants"; -import { getNextLinkFromHeaders, registryRequest } from "../../../utils/registryRequestUtils"; -import { RemoteRepositoryTreeItemBase } from "../RemoteRepositoryTreeItemBase"; -import { RemoteTagTreeItem } from "../RemoteTagTreeItem"; -import { GitLabProjectTreeItem } from "./GitLabProjectTreeItem"; - -export class GitLabRepositoryTreeItem extends RemoteRepositoryTreeItemBase { - public parent: GitLabProjectTreeItem; - public readonly repoId: string; - - private _nextLink?: string; - - public constructor(parent: GitLabProjectTreeItem, id: string, name: string) { - // GitLab returns an empty repository name, - // if the project's namespace is the same as the repository - super(parent, name || parent.label); - this.repoId = id; - this.id = this.repoId; - } - - public async loadMoreChildrenImpl(clearCache: boolean, context: IActionContext): Promise { - if (clearCache) { - this._nextLink = undefined; - } - - const url = this._nextLink || `api/v4/projects/${this.parent.projectId}/registry/repositories/${this.repoId}/tags?per_page=${PAGE_SIZE}`; - const response = await registryRequest(this, 'GET', url); - this._nextLink = getNextLinkFromHeaders(response); - return await this.createTreeItemsWithErrorHandling( - response.body, - 'invalidTag', - async t => { - const details = await this.getTagDetails(t.name); - return new RemoteTagTreeItem(this, t.name, details.created_at); - }, - t => t.name - ); - } - - public hasMoreChildrenImpl(): boolean { - return !!this._nextLink; - } - - private async getTagDetails(tag: string): Promise { - const url = `api/v4/projects/${this.parent.projectId}/registry/repositories/${this.repoId}/tags/${tag}`; - const response = await registryRequest(this, 'GET', url); - return response.body; - } -} - -interface ITag { - name: string; -} - -interface ITagDetails { - // eslint-disable-next-line @typescript-eslint/naming-convention - created_at: string; -} diff --git a/src/tree/registries/gitLab/gitLabRegistryProvider.ts b/src/tree/registries/gitLab/gitLabRegistryProvider.ts deleted file mode 100644 index 827ef762b7..0000000000 --- a/src/tree/registries/gitLab/gitLabRegistryProvider.ts +++ /dev/null @@ -1,25 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { l10n } from 'vscode'; -import { RegistryApi } from "../all/RegistryApi"; -import { IRegistryProvider } from "../IRegistryProvider"; -import { deleteRegistryPassword, setRegistryPassword } from '../registryPasswords'; -import { GitLabAccountTreeItem } from "./GitLabAccountTreeItem"; - -export const gitLabRegistryProvider: IRegistryProvider = { - label: "GitLab", - id: 'gitLab', - api: RegistryApi.GitLabV4, - connectWizardOptions: { - wizardTitle: l10n.t('Sign in to GitLab'), - includeUsername: true, - includePassword: true, - passwordPrompt: l10n.t('GitLab Personal Access Token (requires `api` or `read_api` scope)'), - }, - treeItemFactory: (parent, cachedProvider) => new GitLabAccountTreeItem(parent, cachedProvider), - persistAuth: async (cachedProvider, secret) => await setRegistryPassword(cachedProvider, secret), - removeAuth: async (cachedProvider) => await deleteRegistryPassword(cachedProvider), -}; diff --git a/src/tree/registries/registryContextValues.ts b/src/tree/registries/registryContextValues.ts deleted file mode 100644 index 57f10dcdc4..0000000000 --- a/src/tree/registries/registryContextValues.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AzExtTreeItem } from "@microsoft/vscode-azext-utils"; -import { l10n } from 'vscode'; -import { RegistryApi } from "./all/RegistryApi"; -import { azureRegistryProviderId } from "./azure/azureRegistryProvider"; -import { dockerHubRegistryProviderId } from "./dockerHub/dockerHubRegistryProvider"; -import { ICachedRegistryProvider } from "./ICachedRegistryProvider"; -import { IRegistryProviderTreeItem } from "./IRegistryProviderTreeItem"; - -export const registryProviderSuffix = 'RegistryProvider'; -export const registrySuffix = 'Registry'; -export const repositorySuffix = 'Repository'; -export const tagSuffix = 'Tag'; - -export const contextValueSeparator = ';'; -export const anyContextValuePart = '.*'; - -export function getRegistryContextValue(node: AzExtTreeItem & Partial, ...suffixes: string[]): string { - const cachedProvider = getCachedProvider(node); - const parts = [cachedProvider.id, cachedProvider.api, ...suffixes]; - return parts.join(contextValueSeparator) + contextValueSeparator; -} - -/** - * Regular expressions used for the Tree Item Picker (which is used when a command is called from the command palette) - * Registry providers only need to add an entry here if they support commands unique to their provider - */ -export const registryExpectedContextValues = { - all: getRegistryExpectedContextValues({}), - azure: getRegistryExpectedContextValues({ id: azureRegistryProviderId }), - dockerHub: getRegistryExpectedContextValues({ id: dockerHubRegistryProviderId }), - dockerV2: getRegistryExpectedContextValues({ api: RegistryApi.DockerV2 }), -}; - -function getRegistryExpectedContextValues(provider: Partial): { registryProvider: RegExp, registry: RegExp, repository: RegExp, tag: RegExp } { - return { - registryProvider: convertToRegExp(provider, registryProviderSuffix), - registry: convertToRegExp(provider, registrySuffix), - repository: convertToRegExp(provider, repositorySuffix), - tag: convertToRegExp(provider, tagSuffix) - }; -} - -function convertToRegExp(provider: Partial, suffix: string): RegExp { - const parts = [provider.id, provider.api, suffix].map(p => p || anyContextValuePart); - const value = parts.join(contextValueSeparator) + contextValueSeparator; - return new RegExp(value.replace(/undefined/g, anyContextValuePart), 'i'); -} - -function getCachedProvider(node: AzExtTreeItem & Partial): ICachedRegistryProvider { - while (!node.cachedProvider) { - if (!node.parent) { - throw new Error(l10n.t('Failed to find cachedProvider')); - } else { - node = node.parent; - } - } - - return node.cachedProvider; -} diff --git a/src/tree/registries/registryPasswords.ts b/src/tree/registries/registryPasswords.ts deleted file mode 100644 index d02f3b279c..0000000000 --- a/src/tree/registries/registryPasswords.ts +++ /dev/null @@ -1,28 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as crypto from 'crypto'; -import { ext } from '../../extensionVariables'; -import { ICachedRegistryProvider } from "./ICachedRegistryProvider"; - -export async function getRegistryPassword(cached: ICachedRegistryProvider): Promise { - return ext.context.secrets.get(getRegistryPasswordKey(cached)); -} - -export async function setRegistryPassword(cached: ICachedRegistryProvider, password: string): Promise { - return ext.context.secrets.store(getRegistryPasswordKey(cached), password); -} - -export async function deleteRegistryPassword(cached: ICachedRegistryProvider): Promise { - return ext.context.secrets.delete(getRegistryPasswordKey(cached)); -} - -function getRegistryPasswordKey(cached: ICachedRegistryProvider): string { - return getPseudononymousStringHash(cached.id + cached.api + (cached.url || '') + (cached.username || '')); -} - -function getPseudononymousStringHash(s: string): string { - return crypto.createHash('sha256').update(s).digest('hex'); -} diff --git a/src/tree/registries/registryTreeUtils.ts b/src/tree/registries/registryTreeUtils.ts new file mode 100644 index 0000000000..77d99ee9f3 --- /dev/null +++ b/src/tree/registries/registryTreeUtils.ts @@ -0,0 +1,109 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CommonRegistry, CommonRepository, CommonTag, isDockerHubRegistry, isGitHubRegistry, isRegistry, isRepository, isTag } from "@microsoft/vscode-docker-registries"; +import { l10n } from "vscode"; +import { getResourceGroupFromId } from "../../utils/azureUtils"; +import { AzureRegistryItem } from "./Azure/AzureRegistryDataProvider"; + +/** + * Returns the image name from a registry tag item + * ex: hello-world:latest + */ +export function getImageNameFromRegistryTagItem(tag: CommonTag): string { + if (!isTag(tag) || !isRepository(tag.parent)) { + throw new Error(l10n.t('Unable to get image name')); + } + + const repository = tag.parent as CommonRepository; + return `${repository.label.toLowerCase()}:${tag.label.toLowerCase()}`; +} + +/** + * Returns the base image path from a registry + * ex: docker.io/library (Docker Hub) + * myregistry.azurecr.io (Azure) + * ghcr.io/library (GitHub) + * localhost:5000 (Local) + */ +export function getBaseImagePathFromRegistry(registry: CommonRegistry): string { + if (!isRegistry(registry)) { + throw new Error(l10n.t('Unable to get base image path')); + } + + const baseUrl = registry.baseUrl.authority; + + if (isDockerHubRegistry(registry) || isGitHubRegistry(registry)) { + return `${baseUrl}/${registry.label}`; + } + + return baseUrl; +} + +/** + * Returns the full image name from a registry tag item + * + * ex: docker.io/library/hello-world:latest (Docker Hub) + * myregistry.azurecr.io/hello-world:latest (Azure) + * ghcr.io/myregistry/hello-world:latest (GitHub) + * localhost:5000/hello-world:latest (Local) + */ +export function getFullImageNameFromRegistryTagItem(tag: CommonTag): string { + if (!isTag(tag) || !isRegistry(tag.parent.parent)) { + throw new Error(l10n.t('Unable to get full image name')); + } + + const baseImageName = getBaseImagePathFromRegistry(tag.parent.parent); + let imageName = getImageNameFromRegistryTagItem(tag); + + // For GitHub, the image name is prefixed with the registry name so we + // need to remove it since it is already in the base image name + if (isGitHubRegistry(tag.parent.parent)) { + const regex = /\/(.*)$/; // Match "/" followed by anything until the end + const match = imageName.match(regex); + if (match) { + imageName = match[1]; + } + } + + return `${baseImageName}/${imageName}`; +} + +/** + * Returns the full repository name from a registry repository item + * ex: docker.io/library/hello-world (Docker Hub) + * myregistry.azurecr.io/hello-world (Azure) + * ghcr.io/myregistry/hello-world (GitHub) + * localhost:5000/hello-world (Local) + */ +export function getFullRepositoryNameFromRepositoryItem(repository: CommonRepository): string { + if (!isRepository(repository) || !isRegistry(repository.parent)) { + throw new Error(l10n.t('Unable to get full repository name')); + } + + let imageName = repository.label.toLowerCase(); + const baseImageName = getBaseImagePathFromRegistry(repository.parent); + // For GitHub, the image name is prefixed with the registry name so we + // need to remove it since it is already in the base image name + if (isGitHubRegistry(repository.parent)) { + const regex = /\/(.*)$/; // Match "/" followed by anything until the end + const match = imageName.match(regex); + if (match) { + imageName = match[1]; + } + } + return `${baseImageName}/${imageName}`; +} + +/** + * Returns the resource group from an Azure registry item + */ +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/AzureAccountExtensionListener.ts b/src/utils/AzureAccountExtensionListener.ts deleted file mode 100644 index ee099096f0..0000000000 --- a/src/utils/AzureAccountExtensionListener.ts +++ /dev/null @@ -1,55 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, Event, EventEmitter, extensions } from "vscode"; - -export class AzureAccountExtensionListener extends Disposable { - - private static readonly extensionName: string = 'ms-vscode.azure-account'; - private static extensionInstalledEmitter: EventEmitter; - private static extensionsChangeEventListener: Disposable; - - public static get onExtensionInstalled(): Event { - // Subscribe to extensions change event only once. - if (!AzureAccountExtensionListener.extensionsChangeEventListener) { - AzureAccountExtensionListener.extensionsChangeEventListener = this.subscribeToExtensionsChange(); - } - - // If there were any previous subscribers to this event, dispose them so the old subscriber will not receive the event. - if (AzureAccountExtensionListener.extensionInstalledEmitter) { - AzureAccountExtensionListener.extensionInstalledEmitter.dispose(); - } - AzureAccountExtensionListener.extensionInstalledEmitter = new EventEmitter(); - return this.extensionInstalledEmitter.event; - } - - private static subscribeToExtensionsChange(): Disposable { - const listener: Disposable = extensions.onDidChange(() => { - if (this.isExtensionInstalled(AzureAccountExtensionListener.extensionName)) { - // Once the extension is installed, no need to continue listening for the event. - AzureAccountExtensionListener.extensionInstalledEmitter.fire(true); - AzureAccountExtensionListener.extensionInstalledEmitter.dispose(); - listener.dispose(); - AzureAccountExtensionListener.extensionsChangeEventListener = undefined; - } - }); - return listener; - } - - private static isExtensionInstalled(extensionName: string): boolean { - return extensions.getExtension(AzureAccountExtensionListener.extensionName) !== undefined; - } - - public static dispose(): void { - if (AzureAccountExtensionListener.extensionInstalledEmitter) { - AzureAccountExtensionListener.extensionInstalledEmitter.dispose(); - AzureAccountExtensionListener.extensionInstalledEmitter = undefined; - } - if (AzureAccountExtensionListener.extensionsChangeEventListener) { - AzureAccountExtensionListener.extensionsChangeEventListener.dispose(); - AzureAccountExtensionListener.extensionsChangeEventListener = undefined; - } - } -} diff --git a/src/utils/azureUtils.ts b/src/utils/azureUtils.ts index 25c9bf7977..0125b7c6cc 100644 --- a/src/utils/azureUtils.ts +++ b/src/utils/azureUtils.ts @@ -3,13 +3,9 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ISubscriptionContext } from '@microsoft/vscode-azext-utils'; -import { Request } from 'node-fetch'; -import { URLSearchParams } from 'url'; +import type { ContainerRegistryManagementClient } from '@azure/arm-containerregistry'; +import { AzureSubscription } from '@microsoft/vscode-azext-azureauth'; 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); @@ -23,50 +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); } -/* eslint-enable @typescript-eslint/naming-convention */ diff --git a/src/utils/lazyPackages.ts b/src/utils/lazyPackages.ts index ab02bb6912..11edd3fe73 100644 --- a/src/utils/lazyPackages.ts +++ b/src/utils/lazyPackages.ts @@ -34,13 +34,3 @@ export async function getAzExtAppService(): Promise { - return await import('../tree/registries/azure/AzureAccountTreeItem'); -} - -export async function getAzSubTreeItem(): Promise { - return await import('../tree/registries/azure/SubscriptionTreeItem'); -} diff --git a/src/utils/registryExperience.ts b/src/utils/registryExperience.ts new file mode 100644 index 0000000000..8bb6092ddf --- /dev/null +++ b/src/utils/registryExperience.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AzureWizardPromptStep, ContextValueFilterQuickPickOptions, GenericQuickPickStep, IActionContext, PickFilter, QuickPickWizardContext, RecursiveQuickPickStep, runQuickPickWizard } from '@microsoft/vscode-azext-utils'; +import { CommonRegistryItem } from '@microsoft/vscode-docker-registries'; +import { TreeItem } from 'vscode'; +import { ext } from '../extensionVariables'; +import { UnifiedRegistryItem, UnifiedRegistryTreeDataProvider } from '../tree/registries/UnifiedRegistryTreeDataProvider'; + +export interface RegistryFilter { + /** + * This filter will include registry labels that you do want to show in the quick pick. + */ + include?: string[]; + + /** + * This filter will exclude registry labels that you don't want to show in the quick pick. If `exclude` is present, `include` will be ignored. + */ + exclude?: string[]; +} + +export interface RegistryExperienceOptions extends Partial { + // if registryFilter is undefined, all registries will be shown in the quick pick + registryFilter?: RegistryFilter; +} + +export async function registryExperience(context: IActionContext, options?: RegistryExperienceOptions): Promise> { + // get the registry provider unified item + const promptSteps: AzureWizardPromptStep[] = [ + new RegistryQuickPickStep(ext.registriesTree, options) + ]; + + if (options?.contextValueFilter) { + promptSteps.push(new RecursiveQuickPickStep(ext.registriesTree, options as ContextValueFilterQuickPickOptions)); + } + + const unifiedRegistryItem = await runQuickPickWizard>(context, { + hideStepCount: true, + promptSteps: promptSteps, + }); + + return unifiedRegistryItem; +} + +export class RegistryPickFilter implements PickFilter { + public constructor(private readonly options: RegistryExperienceOptions) { } + + public isFinalPick(treeItem: TreeItem, element: unknown): boolean { + if (this.options.contextValueFilter) { + return false; + } + + return this.matchesFilters(treeItem.label as string); + } + + public isAncestorPick(treeItem: TreeItem, element: unknown): boolean { + return this.matchesFilters(treeItem.label as string); + } + + private matchesFilters(treeItemLabel: string): boolean { + if (this.options.registryFilter?.exclude) { + return !this.options.registryFilter.exclude.includes(treeItemLabel); + } else if (this.options.registryFilter?.include) { + return this.options.registryFilter.include.includes(treeItemLabel); + } else { + return true; + } + } +} + +export class RegistryQuickPickStep extends GenericQuickPickStep { + public readonly pickFilter: PickFilter; + + public constructor( + protected readonly treeDataProvider: UnifiedRegistryTreeDataProvider, + protected readonly pickOptions: RegistryExperienceOptions, + ) { + super(treeDataProvider, pickOptions); + this.pickFilter = new RegistryPickFilter(pickOptions); + } +} diff --git a/src/utils/registryRequestUtils.ts b/src/utils/registryRequestUtils.ts deleted file mode 100644 index 5b001886ed..0000000000 --- a/src/utils/registryRequestUtils.ts +++ /dev/null @@ -1,68 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URL } from "url"; -import { ociClientId } from "../constants"; -import { ErrorHandling, RequestLike, RequestOptionsLike, ResponseLike, httpRequest } from './httpRequest'; - -export function getNextLinkFromHeaders(response: IRegistryRequestResponse): string | undefined { - const linkHeader: string | undefined = response.headers.get('link') as string; - if (linkHeader) { - const match = linkHeader.match(/<(.*)>; rel="next"/i); - return match ? match[1] : undefined; - } else { - return undefined; - } -} - -export async function registryRequest( - node: IRegistryAuthTreeItem | IRepositoryAuthTreeItem, - method: 'GET' | 'DELETE' | 'POST', - url: string, - customOptions?: RequestOptionsLike, - errorHandling: ErrorHandling = ErrorHandling.ThrowOnError -): Promise> { - const options = { - method: method, - headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention - 'X-Meta-Source-Client': ociClientId, - }, - ...customOptions, - }; - - const baseUrl = node.baseUrl || (node).parent.baseUrl; - let fullUrl: string = url; - if (!url.startsWith(baseUrl)) { - const parsed = new URL(url, baseUrl); - fullUrl = parsed.toString(); - } - - const response = await httpRequest(fullUrl, options, async (request) => { - if (node.signRequest) { - return node.signRequest(request); - } else { - return (node).parent?.signRequest(request); - } - }, errorHandling); - - return { - body: method !== 'DELETE' ? await response.json() : undefined, - ...response - }; -} - -export interface IRegistryRequestResponse extends ResponseLike { - body: T -} - -export interface IRegistryAuthTreeItem { - signRequest(request: RequestLike): Promise; - baseUrl: string; -} - -export interface IRepositoryAuthTreeItem extends Partial { - parent: IRegistryAuthTreeItem; -}