pulumi / pulumi-policy

Pulumi's Policy as Code SDK, CrossGuard. Define infrastructure checks in code to enforce security, compliance, cost, and other practices, enforced at deployment time.
https://www.pulumi.com/docs/guides/crossguard/
Apache License 2.0
31 stars 4 forks source link

`validateResourceOfType` doesn't work for a `Provider` resource. #323

Closed ringods closed 2 months ago

ringods commented 6 months ago

What happened?

For a customer case, I tried writing a policy that enforces the subscriptionId to be set on the Azure provider. I can make this work if I write a custom validation fucntion which checks args.type === 'pulumi:providers:azure' manually.

Using validateResourceOfType(azure.Provicer, ... ) doesn't seem to work. In the example below, only the file args-my.json gets written, not args-typed.json.

Example

import * as fs from "fs";

import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";
import { PolicyPack, validateResourceOfType, ResourceValidationArgs, ReportViolation } from "@pulumi/policy";

function myProviderValidation(args: ResourceValidationArgs, reportViolation: ReportViolation): Promise<void> | void {
    if (args.type === 'pulumi:providers:azure') {
        const data = JSON.stringify(args);

        fs.writeFileSync('/Users/ringods/Projects/pulumi-customers/refresh-with-provider-config/policy/args-my.json', data, {
            flag: 'w',
        });
    }
}

const typedProviderValidation = validateResourceOfType(azure.Provider, (provider, args, reportViolation) => {
    const data = JSON.stringify(args);

    fs.writeFileSync('/Users/ringods/Projects/pulumi-customers/refresh-with-provider-config/policy/args-typed.json', data, {
        flag: 'w',
    });
})

new PolicyPack("azure-provider-config", {
    policies: [
        {
            name: "azure-provider-require-subscriptionid-my",
            description: "Checks that an explicit subscriptionId is configured for the stack.",
            enforcementLevel: "mandatory",
            validateResource: myProviderValidation,
        },
        {
            name: "azure-provider-require-subscriptionid-typed",
            description: "Checks that an explicit subscriptionId is configured for the stack.",
            enforcementLevel: "mandatory",
            validateResource: typedProviderValidation,
        },
    ],
});

Output of pulumi about

pulumi about
CLI          
Version      3.100.0
Go Version   go1.21.5
Go Compiler  gc

Plugins
NAME    VERSION
azure   5.60.0
nodejs  unknown

Host     
OS       darwin
Version  14.2
Arch     arm64

This project is written in nodejs: executable='/Users/ringods/.volta/bin/node' version='v18.15.0'

Current Stack: team-ce/refresh-with-provider-config/ringo

TYPE                                    URN
pulumi:pulumi:Stack                     urn:pulumi:ringo::refresh-with-provider-config::pulumi:pulumi:Stack::refresh-with-provider-config-ringo
pulumi:providers:azure                  urn:pulumi:ringo::refresh-with-provider-config::pulumi:providers:azure::default_5_60_0
azure:core/resourceGroup:ResourceGroup  urn:pulumi:ringo::refresh-with-provider-config::azure:core/resourceGroup:ResourceGroup::resourceGroup
azure:storage/account:Account           urn:pulumi:ringo::refresh-with-provider-config::azure:storage/account:Account::storage

Found no pending operations associated with team-ce/ringo

Backend        
Name           pulumi.com
URL            https://app.pulumi.com/v-ringo-pulumi-corp
User           v-ringo-pulumi-corp
Organizations  v-ringo-pulumi-corp, team-ce, ediri, demo, pulumi
Token type     personal

Dependencies:
NAME            VERSION
@pulumi/azure   5.60.0
@pulumi/pulumi  3.100.0
@types/node     18.19.4

Pulumi locates its logs in /var/folders/yq/10j1hf8s1ks9f23yxrdbbbb40000gn/T/ by default

Additional context

Test this with this small Azure Classic app:

import * as pulumi from "@pulumi/pulumi";
import * as azure from "@pulumi/azure";

// Create an Azure Resource Group
const resourceGroup = new azure.core.ResourceGroup("resourceGroup");

// Create an Azure resource (Storage Account)
const account = new azure.storage.Account("storage", {
    // The location for the storage account will be derived automatically from the resource group.
    resourceGroupName: resourceGroup.name,
    accountTier: "Standard",
    accountReplicationType: "LRS",
});

// Export the connection string for the storage account
export const connectionString = account.primaryConnectionString;

Running with the local policy pack:

pulumi up -v 5 --policy-pack /Users/ringods/Projects/pulumi-customers/policy-enforce-subscriptionid

Contributing

Vote on this issue by adding a 👍 reaction. To contribute a fix for this issue, leave a comment (and link to your pull request, if you've opened one already).

justinvp commented 6 months ago

validateResourceOfType is implemented using this internal helper:

https://github.com/pulumi/pulumi-policy/blob/2fa7cabda942f11b3fd2c1939b975d5a2a7d766c/sdk/nodejs/policy/server.ts#L584-L593

In generated Node.js provider SDKs, it looks like there is a difference in values for __pulumiType for Provider resources vs. regular custom resources. Providers have just have the name of the provider __pulumiType = 'azure' whereas custom resources have a full type token __pulumiType = 'azure:storage/blob:Blob'.

Provider

https://github.com/pulumi/pulumi-azure/blob/0cceaf4dc339b2d3a80acb8b4dc521dc4f4b61d9/sdk/nodejs/provider.ts#L16-L28

    /** @internal */
    public static readonly __pulumiType = 'azure';

    /**
     * Returns true if the given object is an instance of Provider.  This is designed to work even
     * when multiple copies of the Pulumi SDK have been loaded into the same process.
     */
    public static isInstance(obj: any): obj is Provider {
        if (obj === undefined || obj === null) {
            return false;
        }
        return obj['__pulumiType'] === "pulumi:providers:" + Provider.__pulumiType;
    }

Custom Resource

https://github.com/pulumi/pulumi-azure/blob/0cceaf4dc339b2d3a80acb8b4dc521dc4f4b61d9/sdk/nodejs/storage/blob.ts#L57-L69

    /** @internal */
    public static readonly __pulumiType = 'azure:storage/blob:Blob';

    /**
     * Returns true if the given object is an instance of Blob.  This is designed to work even
     * when multiple copies of the Pulumi SDK have been loaded into the same process.
     */
    public static isInstance(obj: any): obj is Blob {
        if (obj === undefined || obj === null) {
            return false;
        }
        return obj['__pulumiType'] === Blob.__pulumiType;
    }

I'd have to double check to see if it's always been like that, but the likely fix would be to adjust the internal isTypeOf helper to strip the pulumi:providers: prefix from the type if it has that prefix.

justinvp commented 6 months ago

@ringods, actually, I can't repro this. I tried to repro with Azure classic program and policy pack, and the policy using validateResourceOfType(azure.Provider runs for me. I also added an integration test in https://github.com/pulumi/pulumi-policy/pull/324 which is passing.

I see your Pulumi program is using @pulumi/azure 5.60.0. What version of @pulumi/azure is the policy pack using?

ringods commented 6 months ago

@justinvp I instantiated a new policy pack from template azure-classic-typescript and got this as the azure classic provider:

        "@pulumi/azure": "^4.0.0",

https://github.com/pulumi/templates-policy/blob/ad8312ce35a33f7e5dd1f9b74444817441fbbcb2/azure-classic-typescript/package.json#L5

A number of the policy templates haven't had their dependencies updated in years. 😮

When I update @pulumi/azure to exactly 5.0.0, it still fails, but when bumping to 5.60.0 it works. So the type check is probably updated somewhere along the v5 releases.

justinvp commented 2 months ago

This should be fixed with https://github.com/pulumi/templates-policy/pull/27.

pulumi-bot commented 2 months ago

Cannot close issue:

Please fix these problems and try again.