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.

Home Page:https://www.pulumi.com/docs/guides/crossguard/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

checking `kubernetes:yaml:ConfigGroup` resources

errordeveloper opened this issue · comments

I am creating a GKE cluser and using kubernetes:yaml:ConfigGroup to deploy some workload to the cluster.

Before I create any resources at all, my policy that looks for certain specific resources doesn't work, albeit it does later on, if cluster was already deployed.

So, in other words, when preview output look like this, it doesn't work:


     Type                                                               Name                                        Status              Info
     pulumi:pulumi:Stack                                                ilya-dev                      (668s)             2 messages
 +   ├─ gcp:container:Cluster                                           ilya-dev-1                              created (666s)      
 +   ├─ gcp:container:NodePool                                          ilya-dev-1-np-0                         created (111s)      
 +   ├─ pulumi:providers:kubernetes                                     gke-provider                                created (0.40s)     
     └─ kubernetes:yaml:ConfigGroup                                     flux-install                                 (0.00s)       

But it works when the kubernetes:yaml:ConfigGroup resource has dependents in the preview output:


     pulumi:pulumi:Stack                                                ilya-dev                      (668s)             2 messages
 +   ├─ gcp:container:Cluster                                           ilya-dev-1                              created (666s)      
 +   ├─ gcp:container:NodePool                                          ilya-dev-1-np-0                         created (111s)      
 +   ├─ pulumi:providers:kubernetes                                     gke-provider                                created (0.40s)     
     └─ kubernetes:yaml:ConfigGroup                                     flux-install                                 (0.00s)            
 +      ├─ kubernetes:core/v1:ServiceAccount                            flux-system/helm-controller                 created (3s)        
 +      ├─ kubernetes:core/v1:Namespace                                 flux-system                                 created (0.58s)     
 +      ├─ kubernetes:core/v1:ServiceAccount                            flux-system/kustomize-controller            created (1s)        
 +      ├─ kubernetes:core/v1:Service                                   flux-system/notification-controller         created (131s)      
 +      ├─ kubernetes:networking.k8s.io/v1:NetworkPolicy                flux-system/allow-scraping                  created (1s)        
 +      ├─ kubernetes:core/v1:ServiceAccount                            flux-system/notification-controller         created (1s)        
 +      ├─ kubernetes:core/v1:ServiceAccount                            flux-system/source-controller               created (2s)        
 +      ├─ kubernetes:networking.k8s.io/v1:NetworkPolicy                flux-system/allow-webhooks                  created (3s)        
 +      ├─ kubernetes:apps/v1:Deployment                                flux-system/notification-controller         created (166s)      
 +      ├─ kubernetes:core/v1:Service                                   flux-system/source-controller               created (149s)      
 +      ├─ kubernetes:apps/v1:Deployment                                flux-system/helm-controller                 created (166s)      
 +      ├─ kubernetes:networking.k8s.io/v1:NetworkPolicy                flux-system/allow-egress                    created (6s)        
 +      ├─ kubernetes:rbac.authorization.k8s.io/v1:ClusterRole          crd-controller-flux-system                  created (7s)        
 +      ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition  receivers.notification.toolkit.fluxcd.io    created (8s)        
 +      ├─ kubernetes:apps/v1:Deployment                                flux-system/kustomize-controller            created (165s)      
 +      ├─ kubernetes:apps/v1:Deployment                                flux-system/source-controller               created (168s)      
 +      ├─ kubernetes:rbac.authorization.k8s.io/v1:ClusterRoleBinding   cluster-reconciler-flux-system              created (11s)       
 +      ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition  alerts.notification.toolkit.fluxcd.io       created (11s)       
 +      ├─ kubernetes:core/v1:Service                                   flux-system/webhook-receiver                created (136s)      
 +      ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition  ocirepositories.source.toolkit.fluxcd.io    created (12s)       
 +      ├─ kubernetes:rbac.authorization.k8s.io/v1:ClusterRoleBinding   crd-controller-flux-system                  created (12s)       
 +      ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition  providers.notification.toolkit.fluxcd.io    created (12s)       
 +      ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition  helmrepositories.source.toolkit.fluxcd.io   created (12s)       
 +      ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition  buckets.source.toolkit.fluxcd.io            created (13s)       
 +      ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition  helmcharts.source.toolkit.fluxcd.io         created (13s)       
 +      ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition  gitrepositories.source.toolkit.fluxcd.io    created (13s)       
 +      ├─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition  helmreleases.helm.toolkit.fluxcd.io         created (13s)       
 +      └─ kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition  kustomizations.kustomize.toolkit.fluxcd.io  created (13s) 

It will take some effort to make all of the code sharable, but here is what the policy definition looks like at the moment:


import * as gcp from "@pulumi/gcp";
import * as policy from "@pulumi/policy";

import * as kube from "@pulumi/kubernetes";

const stackPolicy: policy.StackValidationPolicy = {
    name: "validate-stack-resources",
    description: "Validate the stack contains correct resources.",
    enforcementLevel: "mandatory",
    validateStack: async(args: policy.StackValidationArgs, fail: policy.ReportViolation) => {
        let networks = args.resources.filter(r => r.isType(gcp.compute.Network));
        if (networks.length != 1) {
            fail(`expected one network, found ${networks.length}`)
        }

        let subnetworks = args.resources.filter(r => r.isType(gcp.compute.Subnetwork));
        if (subnetworks.length != 1) {
            fail(`expected one subnetwork, found ${networks.length}`)
        }

        const network = networks[0].asType(gcp.compute.Network);
        const subnetwork = subnetworks[0].asType(gcp.compute.Subnetwork);

        if (network && subnetwork) {
            if (subnetwork.network != network.selfLink) {
                fail("subnetwork is linked to an unexpected network")
            }
        } else {
            fail("missing network or subnetwork")
        }

        let clusters = args.resources.filter(r => r.isType(gcp.container.Cluster));
        if (clusters.length !== 1) {
            fail(`expected one GKE Cluster but found ${clusters.length}`);
        }

        const cluster = clusters[0].asType(gcp.container.Cluster);
        if (cluster) {
            if (cluster.network && network) {
                if (!network.selfLink.endsWith(cluster.network)) {
                    fail("cluster is linked to an unexpected network: "+`${cluster.network} != ${network.selfLink}`)
                }
            }

            let addons = cluster.addonsConfig;
            if (!addons.dnsCacheConfig.enabled) {
                fail("DNS cache addon should be enabled")
            }
            if (addons.horizontalPodAutoscaling.disabled) {
                fail("HPA addon should be enabled")
            }
            if (!addons.httpLoadBalancing.disabled) {
                fail("HTTP LB should be disabled")
            }
            if (!addons.configConnectorConfig.enabled) {
                fail("Config Connector should be enabled")
            }

            if (cluster.datapathProvider != "ADVANCED_DATAPATH") {
                fail("datapath provider should be 'ADVANCED_DATAPATH'")
            }

            if (cluster.podSecurityPolicyConfig?.enabled) {
                fail("PSP should be disabled")
            }

            if (cluster.privateClusterConfig.enablePrivateEndpoint) {
                fail("private endpoints should be disabled")
            }

            if (!cluster.privateClusterConfig.enablePrivateNodes) {
                fail("private ndos shoudl be enabled")
            }

            if (!cluster.removeDefaultNodePool) {
                fail("default node pool should be disabled")
            }
        }

        let fluxResourcesStats = new Map<string, number>(Object.entries({
            "kubernetes:apiextensions.k8s.io/v1:CustomResourceDefinition": 10,
            "kubernetes:apps/v1:Deployment": 4,
            "kubernetes:core/v1:Namespace": 1,
            "kubernetes:core/v1:Service": 3,
            "kubernetes:core/v1:ServiceAccount": 4,
            "kubernetes:networking.k8s.io/v1:NetworkPolicy": 3,
            "kubernetes:rbac.authorization.k8s.io/v1:ClusterRole": 1,
            "kubernetes:rbac.authorization.k8s.io/v1:ClusterRoleBinding": 2,
        }));

        let fluxConfigGroup = args.resources.filter(r => r.isType(kube.yaml.ConfigGroup))

        if (!fluxConfigGroup) {
            let fluxResources = args.resources.filter(r => (r.parent?.name == "flux-install"))
            let fluxResourceCount = 0;
            for (let [k, v] of fluxResourcesStats) {
                let resources = fluxResources.filter(r => (r.type == k))
                fluxResourceCount += resources.length
                if (resources.length != v) {
                    fail(`unexpected number of flux resources of type ${k}: ${resources.length} != ${v}`)
                }
            }
            if (fluxResourceCount != 28) {
                fail(`unexpected total number of flux resources: ${fluxResourceCount}`)
            }
        } else {
            if (fluxConfigGroup.length != 1) {
                fail(`config group length: ${fluxConfigGroup.length}`)
            }
        }
    },
};

const tests = new policy.PolicyPack("infra-policy", {
    policies: [stackPolicy],
});

It would make some sense if I could at least get the group resource itself, which I'm attempting to this way:

        let fluxConfigGroup = args.resources.filter(r => r.isType(kube.yaml.ConfigGroup))

But it doesn't seem to be present at all.

Hello @errordeveloper thanks for opening this. Could you share the output of pulumi about in the comments?

Odd...I would expect r => r.isType(kube.yaml.ConfigGroup) to have at least one member. I wish I had more to offer at this time.

For posterity:

Thanks for getting back to me @RobbieMcKinstry!

I've made a few changes and use load manifests dirctly from files instead of using a package.

$ pulumi about --stack dev2                      
CLI          
Version      3.43.1
Go Version   go1.19.2
Go Compiler  gc

Plugins
NAME        VERSION
gcp         6.40.0
gcp         6.40.0
kubernetes  3.21.4
kubernetes  3.21.4
nodejs      unknown
tls         4.6.1

Host     
OS       darwin
Version  12.6
Arch     arm64

This project is written in nodejs: executable='/Users/ilya/Library/Local/Homebrew/bin/node' version='v18.10.0'

Current Stack: dev2

Found no resources associated with dev2

Found no pending operations associated with dev2

Backend        
Name           pulumi.com
URL            https://app.pulumi.com/errordeveloper
User           errordeveloper
Organizations  errordeveloper, atomisthq

Dependencies:
NAME                      VERSION
jest                      29.1.2
ts-node                   10.9.1
@pulumi/gcp               6.40.0
@pulumi/kubernetes        3.21.4
@pulumi/policy            1.5.0
@pulumi/tls               4.6.1
@types/jest               29.1.2
@babel/preset-env         7.19.4
@babel/preset-typescript  7.18.6
@pulumi/pulumi            3.42.0
yaml                      2.1.3

Pulumi locates its logs in /var/folders/gm/dnc405js6nlb275l4v2gh_b40000gn/T/ by default
$ pulumi up --stack dev2 --policy-pack policy
Previewing update (dev2)

View Live: https://app.pulumi.com/errordeveloper/image-build-service/dev2/previews/c15eaa03-60ff-4541-9db9-0195f2c5e148

     Type                              Name                                                   Plan       Info
 +   pulumi:pulumi:Stack               image-build-service-dev2                               create     1 error
 +   ├─ kubernetes:yaml:ConfigGroup    flux-install                                           create     
 +   │  ├─ kubernetes:yaml:ConfigFile  ../configs/shared/flux/flux.yaml                       create     1 message
 +   │  └─ kubernetes:yaml:ConfigFile  ../configs/shared/flux/sources.yaml                    create     1 message
 +   ├─ gcp:projects:Service           logging-service                                        create     
 +   ├─ gcp:projects:Service           iam-service                                            create     
 +   ├─ gcp:projects:Service           monitoring-service                                     create     
 +   ├─ gcp:projects:Service           cloudkms-service                                       create     
 +   ├─ tls:index:PrivateKey           flux-image-build-service                               create     
 +   ├─ gcp:projects:Service           storage-api-service                                    create     
 +   ├─ gcp:projects:Service           compute-service                                        create     
 +   ├─ gcp:projects:Service           container-service                                      create     
 +   ├─ gcp:projects:Service           iamcredentials-service                                 create     
 +   ├─ gcp:projects:Service           storage-component-service                              create     
 +   ├─ gcp:projects:Service           cloudresourcemanager-service                           create     
 +   ├─ gcp:compute:Network            ilya-ibs-dev-2-network                                 create     
 +   ├─ gcp:serviceAccount:Account     ilya-ibs-dev-2-node-sa                                 create     
 +   ├─ gcp:compute:Router             ilya-ibs-dev-2-router                                  create     
 +   ├─ gcp:compute:Subnetwork         ilya-ibs-dev-2-subnetwork                              create     
 +   ├─ gcp:projects:IAMMember         monitoring-metricwriter-ilya-ibs-dev-2-node-sa-member  create     
 +   ├─ gcp:projects:IAMMember         monitoring-viewer-ilya-ibs-dev-2-node-sa-member        create     
 +   ├─ gcp:projects:IAMMember         logging-logwriter-ilya-ibs-dev-2-node-sa-member        create     
 +   ├─ gcp:compute:RouterNat          ilya-ibs-dev-2-router-nat                              create     
 +   ├─ gcp:container:Cluster          ilya-ibs-dev-2                                         create     
 +   ├─ pulumi:providers:kubernetes    gke-provider                                           create     
 +   └─ gcp:container:NodePool         ilya-ibs-dev-2-np-0                                    create     
 
Diagnostics:
  kubernetes:yaml:ConfigFile (../configs/shared/flux/sources.yaml):
    Can't decode yaml config when provider is not fully initialized. This can result in empty previews but should resolve correctly during apply.
 
  pulumi:pulumi:Stack (image-build-service-dev2):
    error: preview failed
 
  kubernetes:yaml:ConfigFile (../configs/shared/flux/flux.yaml):
    Can't decode yaml config when provider is not fully initialized. This can result in empty previews but should resolve correctly during apply.
 
Policy Violations:
    [mandatory]  build-service-infra-policy v0.0.1  validate-stack-resources (pulumi:pulumi:Stack: image-build-service-dev2)
    Validate the stack contains correct resources.
    config group length: 0
    
    [mandatory]  build-service-infra-policy v0.0.1  validate-stack-resources (pulumi:pulumi:Stack: image-build-service-dev2)
    Validate the stack contains correct resources.
    config files length: 0
    
Outputs:
    cluster           : "ilya-ibs-dev-2-4bc8a11"
    fluxPublicKey     : output<string>
    nodeServiceAccount: output<string>
    project           : "sandbox-298914"
    region            : "europe-west2"
$

So now there also Can't decode yaml config when provider is not fully initialized warnings, which is probably to do with CRDs and CRs.
I understand that some things get a bit complex to do with CRDs and CRs... but I still think the object should be visible to the policy.

Here is relevant policy logic:


        let fluxConfigGroup = args.resources.filter(r => r.isType(kube.yaml.ConfigGroup))
        if (fluxConfigGroup.length == 0) {
            fail(`config group length: ${fluxConfigGroup.length}`)
        }

        let fluxConfigFiles = args.resources.filter(r => r.isType(kube.yaml.ConfigFile))
        if (fluxConfigFiles.length == 0) {
            fail(`config files length: ${fluxConfigFiles.length}`)
        }

I also added one kube.core.v1.Secret, but this is dependenat on a private key, so I have this in my code:


const fluxPrivateKey = new PrivateKey("flux-image-build-service", { algorithm: "RSA" });

	let fluxPrivateKeySecret: kube.core.v1.Secret;

	fluxPrivateKey.privateKeyOpenssh.apply(key => {
		fluxPrivateKeySecret = new kube.core.v1.Secret("flux-image-build-service", {
			metadata: {
				// name must be set here to avoid Pulumi auto-naming, as otherwise
				// secret name would have to be passed to a manifest that is currently
				// static (albeit transform could be used for this, it seems unnecessary)
				name: "flux-image-build-service",
				namespace: "flux-system",
			},
			stringData: {
				identity: key.trimEnd(),
			 },
		}, kubeOpts);

I'm trying to write policy logic for this also, and it seems like I'm hitting another bump, probably to do with that the resource is defoined in the promise callback... What happens is that args.resources.filter(r => r.isType(kube.core.v1.Secret)) returns array of one element, but it's undefined.

I guess this leave me wondering wheather I'm really hitting the limitations of how policy is implemented at the moment?