cdk8s-team / cdk8s-core

Define Kubernetes native apps and abstractions using object-oriented programming
Apache License 2.0
64 stars 26 forks source link

How to set fields to `null` literral in go #2562

Open tgermain opened 3 months ago

tgermain commented 3 months ago

Description of the bug:

I'm using cdk8s with go and I'm having trouble setting some fields to null literal.

Why do I need to set fields to null in the first place?

When creating/updating resource using kubectl apply, it does a 3 way merge between the existing k8s resource on the cluster and what you want to apply.

For instance, if you already have a k8s resource with a foo: bar label like:

apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    foo: bar
    baz: baz
  name: my-cm

and you want to remove the foo label, applying a resource like:

apiVersion: v1
kind: ConfigMap
metadata:
  labels:
  # no more foo label
    baz: baz
  name: my-cm

is not enough because of the 3 way merge. It will work if you try to apply the same manifest to a brand new k8s cluster but not on one where the my-cm configmap already have the foo label.

A common trick I've used in the past and that was implemented in https://github.com/kubernetes/kubernetes/pull/40630 is to set the field you want to remove to null literal:

apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    foo: null
    baz: baz
  name: my-cm

This yaml manifest will remove the foo label if it's already present in the existing resource.

Reproduction Steps:

This is my test cdk8s app:

package main

import (
    "fmt"

    "example.com/cdk8s-demo/imports/k8s"
    "github.com/aws/jsii-runtime-go"
    "github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2"
)

func main() {
    app := cdk8s.NewApp(&cdk8s.AppProps{})

    chart := cdk8s.NewChart(app, jsii.String("my-chart"), &cdk8s.ChartProps{})

    labels := map[string]*string{
        "foo": jsii.String("bar"),
        "set-to-null": jsii.String("to-be-replaced"),
    }
    k8s.NewKubeConfigMap(chart, jsii.String("my-cm"), &k8s.KubeConfigMapProps{
        Metadata: &k8s.ObjectMeta{
            Labels: &labels,
        }})
    fmt.Println(*app.SynthYaml())
}

It a go module named example.com/cdk8s-demo/imports/k8s. Under example.com/cdk8s-demo/imports/k8s I've imported k8s api objects using cdk8s import k8s -l go.

When run using go run main.go it produce:

apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    foo: bar
    set-to-null: to-be-replaced
  name: my-chart-my-cm-c841940f

Environment:

Other:

What I have already tried:

Using "set-to-null": jsii.String(""), -> it renders an empty string in the yaml (expected).

Using "set-to-null": jsii.String("null"), -> it renders a "null" string in the yaml (expected).

Using json-patch:

package main

import (
    "fmt"

    "example.com/cdk8s-demo/imports/k8s"
    "github.com/aws/jsii-runtime-go"
    "github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2"
)

func main() {
    app := cdk8s.NewApp(&cdk8s.AppProps{})

    chart := cdk8s.NewChart(app, jsii.String("my-chart"), &cdk8s.ChartProps{})

    labels := map[string]*string{
        "foo": jsii.String("bar"),
        "set-to-null": jsii.String(""),
    }
    cm:=k8s.NewKubeConfigMap(chart, jsii.String("my-cm"), &k8s.KubeConfigMapProps{
        Metadata: &k8s.ObjectMeta{
            Labels: &labels,
        }})

    cm.AddJsonPatch(cdk8s.JsonPatch_Replace(jsii.String("/metadata/labels/set-to-null"),"null"))
    fmt.Println(*app.SynthYaml())
}

cm.AddJsonPatch(cdk8s.JsonPatch_Replace(jsii.String("/metadata/labels/set-to-null"),"null")) -> render a "null" string in yaml cm.AddJsonPatch(cdk8s.JsonPatch_Replace(jsii.String("/metadata/labels/set-to-null"),"")) -> render a "" string in yaml

Using pointer to empty interface:

    var i interface{}
    cm.AddJsonPatch(cdk8s.JsonPatch_Replace(jsii.String("/metadata/labels/set-to-null"), &i))

-> it removes the field completely.

I think a json path like {"op": "replace","path": "/metadata/labels/set-to-null","value": null} should work but I think the null literal in the json patch is lost at some point when go ---call---> jsii ---call---> typescript.


This is :bug: Bug Report

iliapolo commented 3 months ago

Nice one. I think JsonPatch is an ok API for this, assuming it works of course :)

The thing is, it does work in typescript:

cdk8s.ApiObject.of(cm).addJsonPatch(cdk8s.JsonPatch.replace('/metadata/labels/set-to-null', null));

In golang, the offending code is the auto generated validations jsii produces:

func validateJsonPatch_AddParameters(path *string, value interface{}) error {
    if path == nil {
        return fmt.Errorf("parameter path is required, but nil was provided")
    }

    if value == nil {
        return fmt.Errorf("parameter value is required, but nil was provided")
    }

    return nil
}

This sounds like a generic jsii issue, i'll check and update here.

Thanks for reporting this!

tgermain commented 3 months ago

I've tried to make my own version of JsonPatch_Replace in order to bypass the validation that prevent me from passing nil as the value of the patch:

package main

import (
    "fmt"

    "example.com/cdk8s-demo/imports/k8s"
    "github.com/aws/jsii-runtime-go"
    "github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2"

    _jsii_ "github.com/aws/jsii-runtime-go/runtime"
    _init_ "github.com/cdk8s-team/cdk8s-core-go/cdk8s/v2/jsii"
)

func main() {
    app := cdk8s.NewApp(&cdk8s.AppProps{})

    chart := cdk8s.NewChart(app, jsii.String("my-chart"), &cdk8s.ChartProps{})

    labels := map[string]*string{
        "foo":         jsii.String("bar"),
        "set-to-null": jsii.String(""),
    }
    cm := k8s.NewKubeConfigMap(chart, jsii.String("my-cm"), &k8s.KubeConfigMapProps{
        Metadata: &k8s.ObjectMeta{
            Labels: &labels,
        }})

    var i interface{}
    // cm.AddJsonPatch(cdk8s.JsonPatch_Replace(jsii.String("/metadata/labels/set-to-null"), &i))
    cm.AddJsonPatch(no_validation_JsonPatch_Replace(jsii.String("/metadata/labels/set-to-null"), &i))
    fmt.Println(*app.SynthYaml())
}

func no_validation_JsonPatch_Replace(path *string, value interface{}) cdk8s.JsonPatch {
    _init_.Initialize()

    var returns cdk8s.JsonPatch

    _jsii_.StaticInvoke(
        "cdk8s.JsonPatch",
        "replace",
        []interface{}{path, value},
        &returns,
    )

    return returns
}

It doesn't break while going through jsii but it produces:

apiVersion: v1
kind: ConfigMap
metadata:
  labels:
    foo: bar
  name: my-chart-my-cm-c841940f
iliapolo commented 3 months ago

@tgermain I've opened a jsii issue. not much we can do on cdk8s side.