@pagopa/dx-cli 0.18.7 → 0.18.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,12 @@ import { AzureCloudAccountService } from "../cloud-account-service.js";
4
4
  const { queryResources } = vi.hoisted(() => ({
5
5
  queryResources: vi.fn().mockRejectedValue(new Error("Not implemented")),
6
6
  }));
7
+ const { mockProviderGet, mockProviderRegister } = vi.hoisted(() => ({
8
+ mockProviderGet: vi
9
+ .fn()
10
+ .mockResolvedValue({ registrationState: "Registered" }),
11
+ mockProviderRegister: vi.fn().mockResolvedValue({}),
12
+ }));
7
13
  vi.mock("@azure/identity", () => ({
8
14
  DefaultAzureCredential: vi.fn(),
9
15
  }));
@@ -12,6 +18,14 @@ vi.mock("@azure/arm-resourcegraph", () => ({
12
18
  resources = queryResources;
13
19
  },
14
20
  }));
21
+ vi.mock("@azure/arm-resources", () => ({
22
+ ResourceManagementClient: class {
23
+ providers = {
24
+ get: mockProviderGet,
25
+ register: mockProviderRegister,
26
+ };
27
+ },
28
+ }));
15
29
  const test = baseTest.extend({
16
30
  // the empty pattern is required by vitest!!!
17
31
  // eslint-disable-next-line no-empty-pattern
@@ -94,11 +108,12 @@ describe("getTerraformBackend", () => {
94
108
  });
95
109
  });
96
110
  describe("isInitialized", () => {
97
- test("returns true when both bootstrap identity and common Key Vault exist", async ({ cloudAccountService, }) => {
111
+ test("returns true when both bootstrap identity and common Key Vault exist and all providers are registered", async ({ cloudAccountService, }) => {
98
112
  // First call: identity query → found
99
113
  queryResources.mockResolvedValueOnce({ data: [], totalRecords: 1 });
100
114
  // Second call: key vault query → found
101
115
  queryResources.mockResolvedValueOnce({ data: [], totalRecords: 1 });
116
+ // Provider checks → all registered (default mock)
102
117
  const result = await cloudAccountService.isInitialized("sub-1", {
103
118
  name: "dev",
104
119
  prefix: "dx",
@@ -127,4 +142,18 @@ describe("isInitialized", () => {
127
142
  });
128
143
  expect(result).toBe(false);
129
144
  });
145
+ test("returns false when resources exist but a required provider is not registered", async ({ cloudAccountService, }) => {
146
+ // Both resources exist
147
+ queryResources.mockResolvedValueOnce({ data: [], totalRecords: 1 });
148
+ queryResources.mockResolvedValueOnce({ data: [], totalRecords: 1 });
149
+ // One provider is not registered
150
+ mockProviderGet.mockResolvedValueOnce({
151
+ registrationState: "NotRegistered",
152
+ });
153
+ const result = await cloudAccountService.isInitialized("sub-1", {
154
+ name: "dev",
155
+ prefix: "dx",
156
+ });
157
+ expect(result).toBe(false);
158
+ });
130
159
  });
@@ -33,6 +33,24 @@ const graphGroupMembershipResponseSchema = z.object({
33
33
  });
34
34
  export class AzureCloudAccountService {
35
35
  #credential;
36
+ #requiredResourceProviders = [
37
+ "Microsoft.Advisor",
38
+ "Microsoft.AlertsManagement",
39
+ "Microsoft.ApiManagement",
40
+ "Microsoft.App",
41
+ "Microsoft.Authorization",
42
+ "Microsoft.AzureTerraform",
43
+ "Microsoft.Cache",
44
+ "Microsoft.Cdn",
45
+ "Microsoft.ContainerInstance",
46
+ "Microsoft.CostManagement",
47
+ "Microsoft.DBforPostgreSQL",
48
+ "Microsoft.KeyVault",
49
+ "Microsoft.ServiceBus",
50
+ "Microsoft.Sql",
51
+ "Microsoft.Storage",
52
+ "Microsoft.Web",
53
+ ];
36
54
  #resourceGraphClient;
37
55
  constructor(credential) {
38
56
  this.#resourceGraphClient = new ResourceGraphClient(credential);
@@ -126,6 +144,8 @@ export class AzureCloudAccountService {
126
144
  assert.equal(cloudAccount.csp, "azure", "Cloud account must be Azure");
127
145
  assert.ok(isAzureLocation(cloudAccount.defaultLocation), "The default location of the cloud account is not a valid Azure location");
128
146
  const logger = getLogger(["gen", "env"]);
147
+ // Register required resource providers before creating any resources
148
+ await this.#registerProviders(cloudAccount.id);
129
149
  const resourceManagementClient = new ResourceManagementClient(this.#credential, cloudAccount.id);
130
150
  const short = {
131
151
  env: environmentShort[name],
@@ -210,7 +230,7 @@ export class AzureCloudAccountService {
210
230
  | where type == 'microsoft.keyvault/vaults'
211
231
  | where name matches regex @'${keyVaultResourceName}'
212
232
  `;
213
- const [identityResult, keyVaultResult] = await Promise.all([
233
+ const [identityResult, keyVaultResult, areProvidersRegistered] = await Promise.all([
214
234
  this.#resourceGraphClient.resources({
215
235
  query: identityQuery,
216
236
  subscriptions: [cloudAccountId],
@@ -219,8 +239,11 @@ export class AzureCloudAccountService {
219
239
  query: keyVaultQuery,
220
240
  subscriptions: [cloudAccountId],
221
241
  }),
242
+ this.#areProvidersRegistered(cloudAccountId),
222
243
  ]);
223
- const initialized = identityResult.totalRecords > 0 && keyVaultResult.totalRecords > 0;
244
+ const initialized = identityResult.totalRecords > 0 &&
245
+ keyVaultResult.totalRecords > 0 &&
246
+ areProvidersRegistered;
224
247
  const logger = getLogger(["gen", "env"]);
225
248
  logger.debug("subscription {subscriptionId} initialized: {initialized}", {
226
249
  initialized,
@@ -282,6 +305,14 @@ export class AzureCloudAccountService {
282
305
  type: "azurerm",
283
306
  });
284
307
  }
308
+ async #areProvidersRegistered(subscriptionId) {
309
+ const client = new ResourceManagementClient(this.#credential, subscriptionId);
310
+ const results = await Promise.all(this.#requiredResourceProviders.map(async (namespace) => {
311
+ const provider = await client.providers.get(namespace);
312
+ return provider.registrationState === "Registered";
313
+ }));
314
+ return results.every(Boolean);
315
+ }
285
316
  async #getCurrentPrincipalIds() {
286
317
  // Create Graph client with custom auth provider that fetches fresh tokens
287
318
  const graphClient = Client.init({
@@ -318,4 +349,14 @@ export class AzureCloudAccountService {
318
349
  const allPrincipalIds = new Set([userObjectId, ...groupIds]);
319
350
  return allPrincipalIds;
320
351
  }
352
+ async #registerProviders(subscriptionId) {
353
+ const logger = getLogger(["dx-cli", "register-providers"]);
354
+ const client = new ResourceManagementClient(this.#credential, subscriptionId);
355
+ logger.info("Registering {count} resource providers on subscription {subscriptionId}", { count: this.#requiredResourceProviders.length, subscriptionId });
356
+ await Promise.all(this.#requiredResourceProviders.map(async (namespace) => {
357
+ await client.providers.register(namespace);
358
+ logger.debug("Registered provider {namespace}", { namespace });
359
+ }));
360
+ logger.info("All resource providers registered on subscription {subscriptionId}", { subscriptionId });
361
+ }
321
362
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pagopa/dx-cli",
3
- "version": "0.18.7",
3
+ "version": "0.18.8",
4
4
  "type": "module",
5
5
  "description": "A CLI useful to manage DX tools.",
6
6
  "repository": {
@@ -103,6 +103,7 @@ module "azure-{{displayName}}_bootstrap" {
103
103
  key_vault = {
104
104
  name = module.azure-{{displayName}}_core_values.common_key_vault.name
105
105
  resource_group_name = module.azure-{{displayName}}_core_values.common_key_vault.resource_group_name
106
+ use_rbac = true
106
107
  }
107
108
  use_github_app = true
108
109
  }
@@ -7,7 +7,7 @@ repos:
7
7
  exclude: ^.*/(_modules|modules|\.terraform)(/.*)?$
8
8
  files: infra/(resources/prod|repository)
9
9
  - repo: https://github.com/antonbabenko/pre-commit-terraform
10
- rev: "{{ preCommitTerraformVersion }}"
10
+ rev: v{{ preCommitTerraformVersion }}
11
11
  hooks:
12
12
  - id: terraform_tflint
13
13
  args: