Hello! I'm doing a pulumi POC to replace a messy h...
# kubernetes
b
Hello! I'm doing a pulumi POC to replace a messy helm/helmfile setup. There is something I'd like to ask about composition, a 🧵
suppose I have a library that creates a resource, like a secret:
Copy code
const secret = lib.makeSecret
I want to make changes to this secret, lets say add another entry to it:
Copy code
const newEntry = { SOME_SECRET: "foo" }
const secret = lib.makeSecret
how do I add that new entry to the secret? Usually, I would deep merge
Copy code
const updatedSecret = merge(secret,
  { 
    data: newEntry
  }
)
but that doesn't seem to cut it
here is one way I was able to do it: passing on a
transformations
object:
Copy code
// in the library
function makeSecret(..., transformations?: pulumi.ResourceTransformation[]){
  return new k8s.core.v1.Secret(name, spec, { transformations })
}

// in the app code
const newEntry = { foo: "bar" }
makeSecret(..., [
            args => {
                const a = args.props as k8s.core.v1.SecretArgs
                if (a.data) {
                    a.data = pulumi.output(a.data).apply(data => ({
                        ...data,
                        ...newEntry
                    }))
                }
                return { props: a, opts: args.opts }
            }
        ]
q
secrets are outputs (think Promises), map on the secret, merge in the anonymous function
it's
.apply
in pulumi-ts (the equivalent of
.then
I mean)
b
what really got me was the
Input<T>
type. You can assign a plain value to that type
const a: Input<string> = "1"
But, if you want to modify one, you have to transform it into an
Output<T>
first (or so it seems)
that is:
Copy code
const a: pulumi.Input<string> = "a"
const aOutput: pulumi.Input<string> = pulumi.output("a")

const ab: pulumi.Input<string> = a + "b"
const abOutput: pulumi.Input<string> = aOutput + "b"

pulumi.log.info(`ab is: ${ab}`) // prints "ab"
pulumi.log.info(`abOutput is: ${abOutput}`) // prints "Calling [toString] on an [Output<T>] is not supported"
the compiler does not warn you at all that you're doing something that makes no sense
I'd expect that you have to lift into
Input<T>
or get a compile error
I'm coming from a Scala background, maybe my expectations from TS are wrong 😅
q
If you're coming from Scala then you might be interested in https://virtuslab.github.io/besom/. Your intuition that Inputs should auto-lift is correct and that's exactly how it works in Besom.
b
I was originally looking at this project. One thing that was missing for me was the support of
crd2pulumi
tool. We deal a lot with Istio, and having type support is super helpful
q
Noted! Our codegen will support PCL and by extension we hope to support new tool chain that will hopefully replace crd2pulumi, tf2pulumi and such. Anyway, what might interest you is this union type:
type Input = T | Promise<T> | OutputInstance<T>;
If ts is doing what we're doing the Input type should be used to type arguments and autolifting should happen automatically behind covers. The real question is now why are you using Input type in your code? It should be a receiving type used only as a type of arguments to args classes/objects. In user's code you'd usually use Output.
Is your library exposing Input<String> here:
Copy code
const secret = lib.makeSecret
It should be an Output, in Scala terms Output is a monad that wraps any T in async effect and metadata effect that holds information about whether value is known (used in dry run) and whether it's secret).
b
its in the `transformations`:
Copy code
makeSecret(..., [
            args => {
                const a = args.props as k8s.core.v1.SecretArgs
                if (a.data) { // <-- a.data is pulumi.Input<...>
                    a.data = pulumi.output(a.data).apply(data => ({
                        ...data,
                        ...newEntry
                    }))
                }
                return { props: a, opts: args.opts }
            }
        ]
q
Ahhhh
b
yeah ^_^
I was looking for a way to modify a resource that is created by a library, in case you need to do something non-standard, like add a volume to a deployment
This might come in handy I think.
b
thanks, its exactly where I fell:
Knowing this can be helpful because, since
Input<T>
values have many possible representations—a plain value, a promise, or an output—you would normally need to handle all possible cases. By first transforming that value into an
Output<T>
, you can treat it uniformly instead.
so, essentially, to do anything with an
Input<T>
, it has to be transformed into an
Output<T>
?
man, I miss pattern matching 😛
q
You wouldn't be able to patmat on an Input in Besom either, it's an opaque type over a long union type. We have an extension method defined on it called
.asOutput()
that our codegened
apply
for all the args calls for each argument. In Besom transformations won't expose
Input
type because what you will see are case classes filled with respective Outputs. Then you could just
.copy()
and replace fields. Transformations are TBD for 0.3.0 I think because we need to design a way to replace only what user patmats on in immutable object trees, probably using lenses.
b
Lenses was what I was reaching for too in TS lol, then I remembered in-place mutation is a thing 🤯
quicklens is really nice to work with
q
Yeah, it is, I'm just not sure it's the way to go for us as we would have to have lenses for each and every possible field in all args in every package. Code bloat alone would make it infeasible. I think a custom macro will do, those are, essentially, just Products, and Scala 3 Mirrors handle them beautifully.
b
Mirrors do make life so much better
thanks for looking into this @quaint-spring-93350!
q
no prob!