VirtusLab / besom

Besom - a Pulumi SDK for Scala. Also, incidentally, a broom made of twigs tied round a stick. Brooms and besoms are used for protection, to ward off evil spirits, and cleansing of ritual spaces.
https://virtuslab.github.io/besom/
Apache License 2.0
114 stars 7 forks source link

Kubernetes Secret requires both `stringData` and `data` to work properly #383

Closed pawelprazak closed 4 months ago

pawelprazak commented 4 months ago
check failed because malformed resource inputs: malformed RPC secret: missing value for "data": malformed RPC secret: missing value for "data"

Workaround: define both stringData and data until this problem is solved.

pawelprazak commented 4 months ago

I was able to minify and reproduce the error in an integration test.

While reading the provider schema I've found this interesting info about stringData being "special":

stringData allows specifying non-binary secret data in string form. 
It is provided as a write-only input field for convenience. 
All keys and values are merged into the data field on write, 
overwriting any existing values. The stringData field is never 
output when reading from the API.

While reading the implementation I've found the probable source of the error message, deep in the deserialization logic.

        case resource.SecretSig:
            value, ok := obj["value"]
            if !ok {
                return nil, fmt.Errorf("malformed RPC secret: missing value for %q", key)
            }
            return unmarshalSecretPropertyValue(value, opts), nil

IIUC Pulumi Outputs that are secrets are required to have the value on the wire level.

pawelprazak commented 4 months ago

Interesting thing happened. After adding minified version of the Secret from kubernetes provider:

  test("#383 regression") {
    given Context = DummyContext().unsafeRunSync()
    val e         = summon[Encoder[Output[Secret]]]

    val (_, encoded) = e.encode(Secret("name", SecretArgs())).unsafeRunSync()

    assertEqualsValue(encoded, Null)
  }

object Regression383Test:
  final case class Secret private (
    urn: besom.types.Output[besom.types.URN],
    id: besom.types.Output[besom.types.ResourceId],
    data: besom.types.Output[Map[String, String]]
  ) extends besom.CustomResource
  object Secret extends besom.ResourceCompanion[Secret]:
    def apply(using ctx: besom.types.Context)(
      name: besom.util.NonEmptyString,
      args: SecretArgs = SecretArgs()
    ): besom.types.Output[Secret] =
      ctx.readOrRegisterResource[Secret, SecretArgs](typeToken, name, args, besom.CustomResourceOptions())

    private[besom] def typeToken: besom.types.ResourceType = "kubernetes:core/v1:Secret"
    given resourceDecoder(using besom.types.Context): besom.types.ResourceDecoder[Secret] =
      besom.internal.ResourceDecoder.derived[Secret]
    given decoder(using besom.types.Context): besom.types.Decoder[Secret] =
      besom.internal.Decoder.customResourceDecoder[Secret]

  final case class SecretArgs private (
    data: besom.types.Output[scala.Option[scala.Predef.Map[String, String]]]
  )

  object SecretArgs:
    def apply(
      data: besom.types.Input.Optional[Map[String, besom.types.Input[String]]] = None
    )(using besom.types.Context): SecretArgs =
      new SecretArgs(
        data = data.asOptionOutput(isSecret = true)
      )

  given encoder(using besom.types.Context): besom.types.Encoder[SecretArgs] =
    besom.internal.Encoder.derived[SecretArgs]
  given argsEncoder(using besom.types.Context): besom.types.ArgsEncoder[SecretArgs] =
    besom.internal.ArgsEncoder.derived[SecretArgs]

The test hangs at this line: Screenshot 2024-02-12 at 10 20 51

Screenshot 2024-02-12 at 10 22 07

Given this is unsafeRunSync() I assume it is separate issue.

This exception was printed to the console:

java.lang.NoSuchMethodError: 'com.google.protobuf.struct.Value$Kind$StructValue com.google.protobuf.struct.Value$Kind$StructValue$.unapply(com.google.protobuf.struct.Value$Kind$StructValue)'
    at besom.internal.PropertiesSerializer$.detectUnknowns(PropertiesSerializer.scala:29)
    at besom.internal.PropertiesSerializer$.serializeFilteredProperties$$anonfun$1(PropertiesSerializer.scala:23)
    at besom.internal.Result.map$$anonfun$1(Result.scala:201)
    at besom.internal.Result.flatMap$$anonfun$1(Result.scala:164)
    at besom.internal.Result.runM$$anonfun$7(Result.scala:269)
    at besom.internal.Runtime.flatMapBothM$$anonfun$1$$anonfun$1(Result.scala:118)
    at besom.internal.FutureRuntime.flatMapBoth$$anonfun$1(Result.scala:372)
    at scala.concurrent.impl.Promise$Transformation.run$$$capture(Promise.scala:477)
    at scala.concurrent.impl.Promise$Transformation.run(Promise.scala)
    at java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1423)
    at java.base/java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:387)
    at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java)
    at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312)
    at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843)
    at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808)
    at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)
lbialy commented 4 months ago

It's test scope, right? It probably uses global EC and that's why it hangs.

On Mon 12. Feb 2024 at 10:23, Paweł Prażak @.***> wrote:

Interesting thing happened. After adding minified version of the Secret from kubernetes provider:

test("#383 regression") { given Context = DummyContext().unsafeRunSync() val e = summon[Encoder[Output[Secret]]]

val (_, encoded) = e.encode(Secret("name", SecretArgs())).unsafeRunSync()

assertEqualsValue(encoded, Null)

} object Regression383Test: final case class Secret private ( urn: besom.types.Output[besom.types.URN], id: besom.types.Output[besom.types.ResourceId], data: besom.types.Output[Map[String, String]] ) extends besom.CustomResource object Secret extends besom.ResourceCompanion[Secret]: def apply(using ctx: besom.types.Context)( name: besom.util.NonEmptyString, args: SecretArgs = SecretArgs() ): besom.types.Output[Secret] = ctx.readOrRegisterResource[Secret, SecretArgs](typeToken, name, args, besom.CustomResourceOptions())

private[besom] def typeToken: besom.types.ResourceType = "kubernetes:core/v1:Secret"
given resourceDecoder(using besom.types.Context): besom.types.ResourceDecoder[Secret] =
  besom.internal.ResourceDecoder.derived[Secret]
given decoder(using besom.types.Context): besom.types.Decoder[Secret] =
  besom.internal.Decoder.customResourceDecoder[Secret]

final case class SecretArgs private ( data: besom.types.Output[scala.Option[scala.Predef.Map[String, String]]] )

object SecretArgs: def apply( data: besom.types.Input.Optional[Map[String, besom.types.Input[String]]] = None )(using besom.types.Context): SecretArgs = new SecretArgs( data = data.asOptionOutput(isSecret = true) )

given encoder(using besom.types.Context): besom.types.Encoder[SecretArgs] = besom.internal.Encoder.derived[SecretArgs] given argsEncoder(using besom.types.Context): besom.types.ArgsEncoder[SecretArgs] = besom.internal.ArgsEncoder.derived[SecretArgs]

The test hangs at this line: Screenshot.2024-02-12.at.10.20.51.png (view on web) https://github.com/VirtusLab/besom/assets/48874/eb0d2ed3-f84c-40ce-84bd-cd66f6f46d51

Screenshot.2024-02-12.at.10.22.07.png (view on web) https://github.com/VirtusLab/besom/assets/48874/066b7957-d305-4318-a42e-54f5079056af

After this exception was printed:

java.lang.NoSuchMethodError: 'com.google.protobuf.struct.Value$Kind$StructValue com.google.protobuf.struct.Value$Kind$StructValue$.unapply(com.google.protobuf.struct.Value$Kind$StructValue)' at besom.internal.PropertiesSerializer$.detectUnknowns(PropertiesSerializer.scala:29) at besom.internal.PropertiesSerializer$.serializeFilteredProperties$$anonfun$1(PropertiesSerializer.scala:23) at besom.internal.Result.map$$anonfun$1(Result.scala:201) at besom.internal.Result.flatMap$$anonfun$1(Result.scala:164) at besom.internal.Result.runM$$anonfun$7(Result.scala:269) at besom.internal.Runtime.flatMapBothM$$anonfun$1$$anonfun$1(Result.scala:118) at besom.internal.FutureRuntime.flatMapBoth$$anonfun$1(Result.scala:372) at scala.concurrent.impl.Promise$Transformation.run$$$capture(Promise.scala:477) at scala.concurrent.impl.Promise$Transformation.run(Promise.scala) at java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1423) at java.base/java.util.concurrent.ForkJoinTask.doExec$$$capture(ForkJoinTask.java:387) at java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java) at java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1312) at java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1843) at java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1808) at java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:188)

Given this is unsafeRunSync() I assume it is separate issue.

— Reply to this email directly, view it on GitHub https://github.com/VirtusLab/besom/issues/383#issuecomment-1938300530, or unsubscribe https://github.com/notifications/unsubscribe-auth/ACBVNUT72JK42EWYWN2OLMDYTHNSZAVCNFSM6AAAAABDCZBAUKVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMYTSMZYGMYDANJTGA . You are receiving this because you are subscribed to this thread.Message ID: @.***>

pawelprazak commented 4 months ago

It's test scope, right? It probably uses global EC and that's why it hangs.

yes it appears so, but I have a strong suspicion that the stack trace is related, because the log trace abruptly ends like this:

2024.02.13 09:19:35:213 scala-execution-context-global-46 [resource: test-secret1[kubernetes:core/v1:Secret]] TRACE besom.internal.ResourceOps.executeRegisterResourceRequest:491
    RegisterResourceRequest for test-secret1[kubernetes:core/v1:Secret]: RegisterResourceRequest(
      type = "kubernetes:core/v1:Secret",
      name = "test-secret1",
      parent = "urn:pulumi:tests-kubernetes-provider-should-work-with-sec5c920e6c2daa3ac41edf1c047b3027dba3196925::kubernetes-secrets::pulumi:pulumi:Stack::kubernetes-secrets-tests-kubernetes-provider-should-work-with-sec5c920e6c2daa3ac41edf1c047b3027dba3196925",
      custom = true,
      object = Some(
        value = Struct(
          fields = HashMap(
            "data" -> Value(
              kind = StructValue(
                value = Struct(
                  fields = Map(
                    "4dabf18193072939515e22adb298388d" -> Value(
                      kind = StringValue(value = "1b47061264138c4ac30d75fd1eb44270"),
                      unknownFields = UnknownFieldSet(fields = Map())
                    ),
                    "value" -> Value(kind = NullValue(value = NULL_VALUE), unknownFields = UnknownFieldSet(fields = Map()))
                  ),
                  unknownFields = UnknownFieldSet(fields = Map())
                )
              ),
              unknownFields = UnknownFieldSet(fields = Map())
            ),
            "metadata" -> Value(
              kind = StructValue(
                value = Struct(
                  fields = Map(
                    "name" -> Value(
                      kind = StringValue(value = "test-secret1"),
                      unknownFields = UnknownFieldSet(fields = Map())
                    )
                  ),
                  unknownFields = UnknownFieldSet(fields = Map())
                )
              ),
              unknownFields = UnknownFieldSet(fields = Map())
            ),
            "stringData" -> Value(
              kind = StructValue(
                value = Struct(
                  fields = Map(
                    "4dabf18193072939515e22adb298388d" -> Value(
                      kind = StringValue(value = "1b47061264138c4ac30d75fd1eb44270"),
                      unknownFields = Unknown
pawelprazak commented 4 months ago

I've compared logs from the engine between besom and TS:

TS:

I0213 13:12:35.720820   16869 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,secret1)]: apiVersion={v1}
I0213 13:12:35.720852   16869 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,secret1)]: kind={Secret}
I0213 13:12:35.720870   16869 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,secret1)]: 4dabf18193072939515e22adb298388d={1b47061264138c4ac30d75fd1eb44270}
I0213 13:12:35.720883   16869 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,secret1)]: password={test-password}
I0213 13:12:35.720893   16869 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,secret1)]: username={test-user}
I0213 13:12:35.721016   16869 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,secret1)]: value={map[password:{test-password} username:{test-user}]}
I0213 13:12:35.721050   16869 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,secret1)]: stringData={&{{map[password:{test-password} username:{test-user}]}}}

besom:

I0213 13:15:24.975319   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: apiVersion={v1}
I0213 13:15:24.975332   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: 4dabf18193072939515e22adb298388d={1b47061264138c4ac30d75fd1eb44270}
I0213 13:15:24.975337   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: value={<nil>}
I0213 13:15:24.975351   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: data={&{{<nil>}}}
I0213 13:15:24.975357   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: kind={Secret}
I0213 13:15:24.975362   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: name={test-secret1}
I0213 13:15:24.975383   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: metadata={map[name:{test-secret1}]}
I0213 13:15:24.975399   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: 4dabf18193072939515e22adb298388d={1b47061264138c4ac30d75fd1eb44270}
I0213 13:15:24.975420   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: password={test-password}
I0213 13:15:24.975432   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: username={test-user}
I0213 13:15:24.975443   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: value={map[password:{test-password} username:{test-user}]}
I0213 13:15:24.975481   17339 rpc.go:292] Unmarshaling property for RPC[ResourceMonitor.RegisterResource(kubernetes:core/v1:Secret,test-secret1)]: stringData={&{{map[password:{test-password} username:{test-user}]}}}
pawelprazak commented 4 months ago

We've mimicked the observer behavior of TS implementation, and added logic to the serializer to skip outputs that are both secret and null.