pulumi / pulumi-kubernetes

A Pulumi resource provider for Kubernetes to manage API resources and workloads in running clusters
https://www.pulumi.com/docs/reference/clouds/kubernetes/
Apache License 2.0
403 stars 115 forks source link

`spec` is missing in output of `CustomResource` #2890

Open adriangb opened 5 months ago

adriangb commented 5 months ago

What happened?

I'm unable to access the spec of a custom resource

Example

const cert = new k8s.apiextensions.CustomResource(
  "cert",
  {
    apiVersion: "cert-manager.io/v1",
    kind: "Certificate",
    spec: {
      secretName: "foo-bar",
      dnsNames: ["example.default.svc.cluster.local"],
      issuerRef: { .. },
    },
  }
)

const secretName = cert.spec.secretName // type error!

Output of pulumi about

❯ pulumi about
CLI
Version 3.111.0 Go Version go1.22.1 Go Compiler gc

Plugins NAME VERSION cloudflare 5.21.0 gcp 7.11.0 google-native 0.32.0 kubernetes 4.8.0 nodejs unknown random 4.15.1 tls 5.0.1

Host
OS darwin Version 14.3.1 Arch arm64

Dependencies: NAME VERSION @pulumi/cloudflare v5.21.0 @pulumi/kubernetes v4.8.0 @pulumi/tls v5.0.1 prettier 3.2.5 @pulumi/google-native v0.32.0 @pulumi/pulumi 3.107.0 eslint-plugin-simple-import-sort 10.0.0 eslint 8.56.0 typescript 5.3.3 @pulumi/gcp v7.11.0 @typescript-eslint/eslint-plugin 6.21.0 @typescript-eslint/parser 6.21.0 eslint-config-prettier 9.1.0 eslint-config-standard 17.1.0 @pulumi/random v4.15.1 @types/node 20.11.20

Additional context

https://pulumi-community.slack.com/archives/CRFURDVQB/p1710688887460189

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).

mjeffryes commented 5 months ago

Thanks @adriangb; yes, while the CustomResource resource type accepts arbitrary input fields, only the apiVersion kind and metadata input properties are copied to the output.

You could potentially work around this limitation by just using the values you passed as inputs?

adriangb commented 5 months ago

It seems like at runtime the input data is copied. I used a @ts-ignore (I’m guessing as any) would work as well.

The problem with using the input values directly is that you need to then add dependsOn and also reference the CustomResource anywhere you want to use the value (and need the CustomResource to be created).

adriangb commented 5 months ago

I’m no typescript expert, but could you make the input generic (requiring the apiVersion and kind keys via an interface or similar) so that the input data is type safe available in the output?

mjeffryes commented 5 months ago

Glad that workaround worked for you. I did some digging on whether we can make the typing better here, and it seems we're a little hamstrung by the fact that CustomResource is a class. It's possible in ts to define a type that copies field names from some other type:

type Copy<Type> = {
  [Property in keyof Type]: Type[Property];
};

So you could imagine doing something like:

type CustomResourceOutputs<T>  = {
    readonly [K in keyof T]: pulumi.Output<any>;
}

export declare class CustomResource<T> extends pulumi.CustomResource implements CustomResourceOutputs<T> {
...
}

but that blows up because "A class can only implement an object type or intersection of object types with statically known members."

FWIW mapped types syntax is not supported directly on the class either:

export declare class CustomResource<T> extends pulumi.CustomResource {
    readonly [K in keyof T]: pulumi.Output<any>;
...
}

(yields lots of syntax errors)

So while we could create a function that does ~ the same work as the constructor for CustomeResource where the fields on output type would reflect the fields on input type, but we can't coerce the type of the class itself to vary based on the inputs.

Probably the best we could do is a wrapper for the type coercion like this:

type CustomResourceOutputs<T>  = {
    readonly [K in keyof T]: pulumi.Output<T[K]>;
} & CustomResource

function wrapper<T extends CustomResourceArgs>(name: string, args: T, opts?: pulumi.CustomResourceOptions): CustomResourceOutputs<T> {
    return new CustomResource(name, args, opts) as CustomResourceOutputs<T>
}

const out = wrapper("foo", { 
  apiVersion : "1",
  kind : "foo",
  spec: {
    bar: "baz"
  }
})

const spec: pulumi.Output<{bar: string}> = out.spec

(There's still some gotchas to work out when some of the input parameters are already pulumi.Outputs, but I think it's theoretically possible.)

adriangb commented 5 months ago

Could you put the input into CustomResource.input but just mark it as Output<InputT>? That way it can be references and the reference works with pulumi’s DAG