pulumi-policy icon indicating copy to clipboard operation
pulumi-policy copied to clipboard

Support ComponentResource in policy

Open clstokes opened this issue 6 years ago • 10 comments

When trying to write a policy against a ComponentResource such as eks.Cluster from @pulumi/eks I get a compilation error.

Code

import * as awsx from "@pulumi/awsx";
import * as eks from "@pulumi/eks";

const vpc = new awsx.ec2.Vpc("vpc", {
    tags: {Name: "my-vpc",}
});

const cluster = new eks.Cluster("main", {
    vpcId: vpc.id,
    subnetIds: vpc.publicSubnetIds,
    desiredCapacity: 1,
    minSize: 1,
    maxSize: 2,
    storageClasses: "gp2",
    deployDashboard: false,
});

Policy Code

import * as awsx from "@pulumi/awsx";
import * as eks from "@pulumi/eks";
import { PolicyPack, typedRule } from "@pulumi/policy";
import * as assert from "assert";

new PolicyPack("eks", {
    policies: [
        {
            name: "awsx",
            description: "awsx-test",
            enforcementLevel: "advisory",
            rules: [
                typedRule(awsx.ec2.Vpc.isInstance, it => {
                    assert.ok(it.tags !== undefined, "tags must be defined.");
                }),
            ],
        },
        {
            name: "eks",
            description: "eks-test",
            enforcementLevel: "advisory",
            rules: [
                typedRule(eks.Cluster.isInstance, it => {
                    assert.ok(it.desiredCapacity > 1, "desiredCapacity should be greater than 1.");
                }),
            ],
        },
    ]
});

Error output

policy-as-code % pulumi policy publish clstokes/k8s
Obtaining policy metadata from policy plugin
error: [runtime] Running program '/Users/clstokes/cc/pulumi/sales/demos/k8s-ts-nginx/policy-as-code' failed with an unhandled exception:

    TSError: ⨯ Unable to compile TypeScript:

index.ts(66,5): error TS1128: Declaration or statement expected.

index.ts(66,6): error TS1128: Declaration or statement expected.

index.ts(67,1): error TS1128: Declaration or statement expected.

index.ts(67,2): error TS1128: Declaration or statement expected.

index.ts(16,34): error TS2339: Property 'tags' does not exist on type 'Pick<ModifyOptionalProperties<{ readonly urn: string; getProvider: never; }>, never>'.

index.ts(26,34): error TS2339: Property 'desiredCapacity' does not exist on type 'Pick<ModifyOptionalProperties<{ readonly urn: string; getProvider: never; }>, never>'.

    at createTSError (/Users/clstokes/cc/pulumi/sales/demos/k8s-ts-nginx/policy-as-code/node_modules/ts-node/src/index.ts:261:12)

    at getOutput (/Users/clstokes/cc/pulumi/sales/demos/k8s-ts-nginx/policy-as-code/node_modules/ts-node/src/index.ts:367:40)

    at Object.compile (/Users/clstokes/cc/pulumi/sales/demos/k8s-ts-nginx/policy-as-code/node_modules/ts-node/src/index.ts:558:11)

    at Module.m._compile (/Users/clstokes/cc/pulumi/sales/demos/k8s-ts-nginx/policy-as-code/node_modules/ts-node/src/index.ts:439:43)

    at Module._extensions..js (internal/modules/cjs/loader.js:789:10)

    at Object.require.extensions.(anonymous function) [as .ts] (/Users/clstokes/cc/pulumi/sales/demos/k8s-ts-nginx/policy-as-code/node_modules/ts-node/src/index.ts:442:12)

    at Module.load (internal/modules/cjs/loader.js:653:32)

    at tryModuleLoad (internal/modules/cjs/loader.js:593:12)

    at Function.Module._load (internal/modules/cjs/loader.js:585:3)

    at Module.require (internal/modules/cjs/loader.js:692:17)

error: policy pack "k8s" failed to start: could not read plugin [/usr/local/bin/pulumi-analyzer-policy] stdout: EOF
policy-as-code %

clstokes avatar Nov 11 '19 18:11 clstokes

The problem is due to awsx.ec2.Vpc and eks.Cluster not having their own static isInstance functions. Since they don't have their own, the one on their base class (ComponentResource) is used, which is defined as:

export class ComponentResource extends Resource {
    /**
     * @internal
     * A private field to help with RTTI that works in SxS scenarios.
     */
    // tslint:disable-next-line:variable-name
    public readonly __pulumiComponentResource: boolean;

    /**
     * Returns true if the given object is an instance of CustomResource.  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 ComponentResource {
        return utils.isInstance<ComponentResource>(obj, "__pulumiComponentResource");
    }

    ...
}

Since the type guard is obj is ComponentResource, the compiler is inferring the type of it as ComponentResource in each of the example policies above instead of their more specific awsx.ec2.Vpc and eks.Cluster types. Since ComponentResource does not have tags or desiredCapacity properties, the compiler is complaining.

With the newer API that looks for the resource's args type from the constructor parameters ("input" shape), instead of using the resource's "output" shape, the compiler is able to pick up the resource's args type for eks.Cluster but picks the wrong args type for awsx.ec2.Vpc (since awsx.ec2.Vpc supports multiple args types), so the compiler still complains about tags there. We should be able to address this by tweaking the aws.ec2.Vpc constructor in a compatible way so the more often used args is the one the compiler infers.

But even with the compiler picking the right types, we still end up calling isInstance at runtime, which will use ComponentResource's isInstance, which isn't going to filter the resource based on type like it does for all the TF and K8s-based resources. To make this work the way we want, we're going to have to add isInstance functions to our component resources that we want to work with policies in a strongly-typed way.

justinvp avatar Nov 12 '19 17:11 justinvp

The "simple" fix here is to add static isInstance functions to our ComponentResources, but it means that anyone who wants to use their ownComponentResources with PaC would have to do that, which isn't ideal. Need to keep thinking about how we could make this work in a more fundamental way that keeps the authoring experience for ComponentResources simple.

justinvp avatar Nov 22 '19 17:11 justinvp

Meant to move this out before the holidays. Still haven't come up with a better, more fundamental, way to workaround the isInstance issue that avoids ComponentResource authors from having to define an isInstance function.

justinvp avatar Jan 06 '20 17:01 justinvp

The short-term workaround would be to add workable isInstance methods on all of our component resources (e.g. in AWSX and EKS), which would be cheap and easy to do.

@lukehoban, for a more fundamental solution, I am wondering if we could somehow leverage the registerProxyConstructor for this, from your multi-language components/libraries prototype PR.

justinvp avatar Feb 05 '20 17:02 justinvp

@lukehoban, for a more fundamental solution, I am wondering if we could somehow leverage the registerProxyConstructor for this, from your multi-language components/libraries prototype PR.

Yes - I do expect that once we land that and enable it for all packages - that will provide what we need here. I do think we should effectively wait on that infrastructure to be available to support this (though it may be a few milestones before it is universally available).

lukehoban avatar Feb 05 '20 19:02 lukehoban

This code compiles for me and cluster shows the right type but the rule is never hit:

import * as eks from "@pulumi/eks";
...
validateResource: validateResourceOfType(eks.Cluster, (cluster, args, reportViolation) => {
  reportViolation('fail');
}

mikhailshilkov avatar Feb 21 '20 15:02 mikhailshilkov

This code compiles for me and cluster shows the right type but the rule is never hit

Right. The problem is the way type checking is done with validateResourceOfType doesn't work for component resources. You'll have to manually check for the type...

Untested, but something like:

validateResource: (args, reportViolation) => {
    if (args.type === "eks:index:Cluster") {
        reportViolation('fail');
    }
}

justinvp avatar Feb 21 '20 15:02 justinvp

Related to this issue appears to be the fact that args.props is empty for component resources. This code:

component_resource_type="custom:resource:Backend"
def component_resource_validator(args: ResourceValidationArgs, report_violation: ReportViolation):
    if args.resource_type == component_resource_type:
        print(f"in component resource: {args.resource_type}")
        print(list(args.props))

produces an empty list - indicating the args.props is empty.

So I can write code that triggers on the component resource type, but I can't check its properties.

MitchellGerdisch avatar Oct 07 '21 15:10 MitchellGerdisch

Related to this issue appears to be the fact that args.props is empty for component resources.

Indeed, this is a limitation of component resources that we'd need to revisit in order for the input properties to be inspectable from policies (and other things like unit tests).

More details: https://github.com/pulumi/pulumi/issues/7822#issuecomment-1003424031

justinvp avatar Aug 30 '22 15:08 justinvp

I've opened https://github.com/pulumi/pulumi/issues/10533 to reconsider passing component resource properties to the engine, so that they'd be available in policies (among other things).

justinvp avatar Aug 30 '22 16:08 justinvp