diff --git a/.github/workflows/release-pullrequest.yaml b/.github/workflows/release-pullrequest.yaml index 20bd115..eb8a71f 100644 --- a/.github/workflows/release-pullrequest.yaml +++ b/.github/workflows/release-pullrequest.yaml @@ -47,6 +47,12 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 + - name: Checkout code + uses: actions/checkout@v4 + + - name: Echo Build Version + run: echo "BUILD_VERSION=${{ github.sha }}" + - name: Build and NOT push id: build uses: docker/build-push-action@v5 @@ -54,6 +60,8 @@ jobs: platforms: ${{ matrix.platform }} push: false labels: ${{ steps.meta.outputs.labels }} + build-args: | + BUILD_VERSION=${{ github.sha }} test: runs-on: ubuntu-latest @@ -61,7 +69,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: 'stable' + go-version: '1.25.3' # same as go.mod - name: Gather dependencies run: go mod download - name: Run coverage diff --git a/.github/workflows/release-tag.yaml b/.github/workflows/release-tag.yaml index fb46a28..66223db 100644 --- a/.github/workflows/release-tag.yaml +++ b/.github/workflows/release-tag.yaml @@ -27,6 +27,12 @@ jobs: pull-requests: write steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Echo Build Version + run: echo "BUILD_VERSION=${{ github.sha }}" + - name: Prepare run: | platform=${{ matrix.platform }} @@ -56,6 +62,8 @@ jobs: push: true labels: ${{ steps.meta.outputs.labels }} outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=true + build-args: | + BUILD_VERSION=${{ github.sha }} - name: Export digest run: | @@ -124,7 +132,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: 'stable' + go-version: '1.25.3' # same as go.mod - name: Gather dependencies run: go mod download - name: Run coverage diff --git a/.ko.yaml b/.ko.yaml index 0220a4e..538cf87 100644 --- a/.ko.yaml +++ b/.ko.yaml @@ -1,12 +1,13 @@ builds: - id: rest-dynamic-controller #main: main.go - dir: . + dir: . env: - CGO_ENABLED=0 ldflags: - -s -w - -extldflags "-static" + - -X main.Build={{.Git.Commit}} defaultPlatforms: - linux/arm64 #- linux/amd64 diff --git a/Dockerfile b/Dockerfile index da95a76..ab2a02a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -24,7 +24,8 @@ COPY internal/ internal/ COPY main.go main.go # Build -RUN CGO_ENABLED=0 GO111MODULE=on go build -a -o /bin/controller ./main.go && \ +ARG BUILD_VERSION +RUN CGO_ENABLED=0 GO111MODULE=on go build -a -ldflags="-X 'main.Build=${BUILD_VERSION}'" -o /bin/controller ./main.go && \ strip /bin/controller # Deployment environment diff --git a/README.md b/README.md index 9048fca..02e0687 100644 --- a/README.md +++ b/README.md @@ -180,6 +180,7 @@ The following environment variables can be configured in the rest-dynamic-contro | Name | Description | Default Value | |------|--------------|---------------| | REST_CONTROLLER_DEBUG | Enable verbose output | `false` | +| REST_CONTROLLER_PRETTY_JSON_DEBUG | Enable pretty-print JSON formatting in HTTP debug output (response bodies) | `false` | | REST_CONTROLLER_WORKERS | Number of worker threads | `1` | | REST_CONTROLLER_RESYNC_INTERVAL | Interval between resyncs | `1m` | | REST_CONTROLLER_GROUP | Resource API group | - | diff --git a/go.mod b/go.mod index db831ba..c629bc6 100644 --- a/go.mod +++ b/go.mod @@ -1,21 +1,20 @@ module github.com/krateoplatformops/rest-dynamic-controller -go 1.25.0 +go 1.25.3 require ( github.com/go-andiamo/splitter v1.2.5 - github.com/go-logr/logr v1.4.2 + github.com/go-logr/logr v1.4.3 github.com/gobuffalo/flect v1.0.3 github.com/google/go-cmp v0.7.0 - github.com/krateoplatformops/plumbing v0.6.1 - github.com/krateoplatformops/snowplow v0.0.0-20250311104630-6e215130151f - github.com/krateoplatformops/unstructured-runtime v0.3.0 + github.com/krateoplatformops/plumbing v0.9.4 + github.com/krateoplatformops/unstructured-runtime v0.3.1 github.com/pb33f/libopenapi v0.28.0 github.com/stretchr/testify v1.11.1 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.33.2 - k8s.io/apimachinery v0.33.2 - k8s.io/client-go v0.33.2 + k8s.io/api v0.34.2 + k8s.io/apimachinery v0.34.2 + k8s.io/client-go v0.34.2 sigs.k8s.io/controller-runtime v0.20.0 sigs.k8s.io/e2e-framework v0.6.0 ) @@ -27,31 +26,27 @@ require ( github.com/buger/jsonparser v1.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emicklei/go-restful/v3 v3.12.1 // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.9.0 // indirect - github.com/fatih/color v1.18.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/go-logr/zapr v1.3.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/google/btree v1.1.3 // indirect - github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/itchyny/gojq v0.12.17 // indirect - github.com/itchyny/timefmt-go v0.1.6 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.20 // indirect github.com/moby/spdystream v0.5.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/pkg/errors v0.9.1 // indirect @@ -62,7 +57,7 @@ require ( github.com/prometheus/procfs v0.15.1 // indirect github.com/speakeasy-api/jsonpath v0.6.2 // indirect github.com/spf13/cobra v1.8.1 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/twmb/murmur3 v1.1.8 // indirect github.com/vladimirvivien/gexe v0.4.1 // indirect @@ -72,30 +67,25 @@ require ( go.opentelemetry.io/otel/trace v1.33.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/mod v0.27.0 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/net v0.43.0 // indirect golang.org/x/oauth2 v0.29.0 // indirect - golang.org/x/sync v0.17.0 // indirect golang.org/x/sys v0.35.0 // indirect golang.org/x/term v0.34.0 // indirect golang.org/x/text v0.29.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.36.0 // indirect - golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated // indirect google.golang.org/protobuf v1.36.5 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect - k8s.io/apiextensions-apiserver v0.32.0 // indirect k8s.io/component-base v0.32.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect - sigs.k8s.io/controller-tools v0.16.5 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect + k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 // indirect + sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect - sigs.k8s.io/yaml v1.4.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) replace github.com/pb33f/libopenapi => github.com/krateoplatformops/libopenapi v0.21.8 diff --git a/go.sum b/go.sum index e71a1b0..8a373af 100644 --- a/go.sum +++ b/go.sum @@ -15,20 +15,18 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= -github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= -github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= -github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/go-andiamo/splitter v1.2.5 h1:P3NovWMY2V14TJJSolXBvlOmGSZo3Uz+LtTl2bsV/eY= github.com/go-andiamo/splitter v1.2.5/go.mod h1:8WHU24t9hcMKU5FXDQb1hysSEC/GPuivIp0uKY1J8gw= -github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= -github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= @@ -43,11 +41,12 @@ github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4 github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg= github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -61,10 +60,6 @@ github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5T github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/itchyny/gojq v0.12.17 h1:8av8eGduDb5+rvEdaOO+zQUjA04MS0m3Ps8HiD+fceg= -github.com/itchyny/gojq v0.12.17/go.mod h1:WBrEMkgAfAGO1LUcGOckBl5O726KPp+OlkKug0I/FEY= -github.com/itchyny/timefmt-go v0.1.6 h1:ia3s54iciXDdzWzwaVKXZPbiXzxxnv1SPGFfM/myJ5Q= -github.com/itchyny/timefmt-go v0.1.6/go.mod h1:RRDZYC5s9ErkjQvTvvU7keJjxUYzIISJGxm9/mAERQg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= @@ -79,36 +74,26 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/krateoplatformops/libopenapi v0.21.8 h1:KKZxXBqkXoWv+/+bVLQiiRZXxHY1Zz+0Yd+ht+ArqWE= github.com/krateoplatformops/libopenapi v0.21.8/go.mod h1:Gc8oQkjr2InxwumK0zOBtKN9gIlv9L2VmSVIUk2YxcU= -github.com/krateoplatformops/plumbing v0.6.1 h1:UxQjxvJwj6ORso/RJePQv7PRC75tCcupCpYZz98BaRI= -github.com/krateoplatformops/plumbing v0.6.1/go.mod h1:mQ/sm0viyKgfR2ARzHuwCpY0rcyMKqCv8a8SOu52yYQ= -github.com/krateoplatformops/snowplow v0.0.0-20250311104630-6e215130151f h1:Kw7J+0uCHPlWmcXeSvstQJDqwMwJ5WgETkHj4Z+UEjI= -github.com/krateoplatformops/snowplow v0.0.0-20250311104630-6e215130151f/go.mod h1:C9UtLN04vcF+Hj79scByTjmwWvIQvAc7i8Dk3N+f6PA= -github.com/krateoplatformops/unstructured-runtime v0.3.0 h1:0lQDUDTViPEBx988b1JJYlVJNwbycTngsyqdaUOzUTQ= -github.com/krateoplatformops/unstructured-runtime v0.3.0/go.mod h1:19uT87wZzRSjrfk3731Xhdt8ww7vnsXhljy4jk0cuWA= +github.com/krateoplatformops/plumbing v0.9.4 h1:VKBKFnmAx9LptJysnkR5SPvW4G6+Dr/SnMTdZvjdpSs= +github.com/krateoplatformops/plumbing v0.9.4/go.mod h1:WOVJKQF2icCphVb1sEgMSvGhMJbigfHM3X6Meqsy4fM= +github.com/krateoplatformops/unstructured-runtime v0.3.1 h1:tQMH19YEJ7+La5283a4FOQlCeBBhS9cqwYzBPW59srs= +github.com/krateoplatformops/unstructured-runtime v0.3.1/go.mod h1:19uT87wZzRSjrfk3731Xhdt8ww7vnsXhljy4jk0cuWA= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= -github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= @@ -133,8 +118,9 @@ github.com/speakeasy-api/jsonpath v0.6.2 h1:Mys71yd6u8kuowNCR0gCVPlVAHCmKtoGXYoA github.com/speakeasy-api/jsonpath v0.6.2/go.mod h1:ymb2iSkyOycmzKwbEAYPJV/yi2rSmvBCLZJcyD+VVWw= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -166,13 +152,15 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ= -golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -184,13 +172,9 @@ golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4= @@ -207,10 +191,6 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg= golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s= -golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= -golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= -golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= -golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -224,41 +204,34 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY= -k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs= +k8s.io/api v0.34.2 h1:fsSUNZhV+bnL6Aqrp6O7lMTy6o5x2C4XLjnh//8SLYY= +k8s.io/api v0.34.2/go.mod h1:MMBPaWlED2a8w4RSeanD76f7opUoypY8TFYkSM+3XHw= k8s.io/apiextensions-apiserver v0.32.0 h1:S0Xlqt51qzzqjKPxfgX1xh4HBZE+p8KKBq+k2SWNOE0= k8s.io/apiextensions-apiserver v0.32.0/go.mod h1:86hblMvN5yxMvZrZFX2OhIHAuFIMJIZ19bTvzkP+Fmw= -k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= -k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E= -k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo= +k8s.io/apimachinery v0.34.2 h1:zQ12Uk3eMHPxrsbUJgNF8bTauTVR2WgqJsTmwTE/NW4= +k8s.io/apimachinery v0.34.2/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= +k8s.io/client-go v0.34.2 h1:Co6XiknN+uUZqiddlfAjT68184/37PS4QAzYvQvDR8M= +k8s.io/client-go v0.34.2/go.mod h1:2VYDl1XXJsdcAxw7BenFslRQX28Dxz91U9MWKjX97fE= k8s.io/component-base v0.32.3 h1:98WJvvMs3QZ2LYHBzvltFSeJjEx7t5+8s71P7M74u8k= k8s.io/component-base v0.32.3/go.mod h1:LWi9cR+yPAv7cu2X9rZanTiFKB2kHA+JjmhkKjCZRpI= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= +k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397 h1:hwvWFiBzdWw1FhfY1FooPn3kzWuJ8tmbZBHi4zVsl1Y= +k8s.io/utils v0.0.0-20250604170112-4c0f3b243397/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.20.0 h1:jjkMo29xEXH+02Md9qaVXfEIaMESSpy3TBWPrsfQkQs= sigs.k8s.io/controller-runtime v0.20.0/go.mod h1:BrP3w158MwvB3ZbNpaAcIKkHQ7YGpYnzpoSTZ8E14WU= -sigs.k8s.io/controller-tools v0.16.5 h1:5k9FNRqziBPwqr17AMEPPV/En39ZBplLAdOwwQHruP4= -sigs.k8s.io/controller-tools v0.16.5/go.mod h1:8vztuRVzs8IuuJqKqbXCSlXcw+lkAv/M2sTpg55qjMY= sigs.k8s.io/e2e-framework v0.6.0 h1:p7hFzHnLKO7eNsWGI2AbC1Mo2IYxidg49BiT4njxkrM= sigs.k8s.io/e2e-framework v0.6.0/go.mod h1:IREnCHnKgRCioLRmNi0hxSJ1kJ+aAdjEKK/gokcZu4k= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= +sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= +sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/internal/controllers/helpers.go b/internal/controllers/helpers.go index 416fe90..36b50c2 100644 --- a/internal/controllers/helpers.go +++ b/internal/controllers/helpers.go @@ -100,3 +100,13 @@ func populateStatusFields(clientInfo *getter.Info, mg *unstructured.Unstructured } return nil } + +// clearStatusFields removes the status field from the Custom Resource. +// This is used during Create and Update operations to ensure no stale fields remain. +func clearStatusFields(mg *unstructured.Unstructured) { + if mg == nil { + return + } + + unstructured.RemoveNestedField(mg.Object, "status") +} diff --git a/internal/controllers/helpers_test.go b/internal/controllers/helpers_test.go index 288ad24..750c8cf 100644 --- a/internal/controllers/helpers_test.go +++ b/internal/controllers/helpers_test.go @@ -806,3 +806,92 @@ func TestPopulateStatusFields(t *testing.T) { }) } } + +func TestClearCRStatusFields(t *testing.T) { + tests := []struct { + name string + mg *unstructured.Unstructured + expected map[string]interface{} + }{ + { + name: "clear status with existing status fields", + mg: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Test", + "metadata": map[string]interface{}{ + "name": "test-resource", + }, + "spec": map[string]interface{}{ + "field1": "value1", + }, + "status": map[string]interface{}{ + "id": "123", + "state": "active", + }, + }, + }, + expected: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Test", + "metadata": map[string]interface{}{ + "name": "test-resource", + }, + "spec": map[string]interface{}{ + "field1": "value1", + }, + }, + }, + { + name: "clear status when no status exists", + mg: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Test", + "spec": map[string]interface{}{ + "field1": "value1", + }, + }, + }, + expected: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Test", + "spec": map[string]interface{}{ + "field1": "value1", + }, + }, + }, + { + name: "clear status with empty status", + mg: &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{"field1": "value1"}, + "status": map[string]interface{}{}, + }, + }, + expected: map[string]interface{}{ + "spec": map[string]interface{}{"field1": "value1"}, + }, + }, + { + name: "nil unstructured - should not panic", + mg: nil, + expected: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + clearStatusFields(tt.mg) + + if tt.mg == nil { + // Test passed if no panic occurred + return + } + + if diff := cmp.Diff(tt.expected, tt.mg.Object); diff != "" { + t.Errorf("clearCRStatusFields() mismatch (-want +got):\n%s", diff) + } + }) + } +} diff --git a/internal/controllers/restResources.go b/internal/controllers/restResources.go index 365ff69..57a444c 100644 --- a/internal/controllers/restResources.go +++ b/internal/controllers/restResources.go @@ -30,7 +30,7 @@ var ( ErrStatusNotFound = errors.New("status not found") ) -func NewHandler(cfg *rest.Config, log logging.Logger, swg getter.Getter, pluralizer pluralizer.PluralizerInterface) controller.ExternalClient { +func NewHandler(cfg *rest.Config, log logging.Logger, swg getter.Getter, pluralizer pluralizer.PluralizerInterface, prettyJSONDebug bool) controller.ExternalClient { dyn, err := dynamic.NewForConfig(cfg) if err != nil { log.Error(err, "Creating dynamic client.") @@ -49,6 +49,7 @@ func NewHandler(cfg *rest.Config, log logging.Logger, swg getter.Getter, plurali dynamicClient: dyn, discoveryClient: dis, swaggerInfoGetter: swg, + prettyJSONDebug: prettyJSONDebug, } } @@ -58,6 +59,7 @@ type handler struct { dynamicClient dynamic.Interface discoveryClient *discovery.DiscoveryClient swaggerInfoGetter getter.Getter + prettyJSONDebug bool } func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (controller.ExternalObservation, error) { @@ -100,6 +102,7 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c // Set client properties cli.Debug = meta.IsVerbose(mg) + cli.PrettyJSON = h.prettyJSONDebug cli.Resource = mg cli.SetAuth = clientInfo.SetAuth cli.IdentifierFields = clientInfo.Resource.Identifiers // TODO: probably redundant since we pass the resource too (`cli.Resource = mg`) @@ -157,6 +160,8 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c // typically searching in the items returned by a "list" API call (e.g GET /resources). // This is typically used when the resource does not have an server-side generated identifier (e.g., ID, UUID) yet, // for instance before creation (in the first ever reconcile loop). + + // This branch is also hit when the resource does not support a `get` action at all but supports only a `findby` action for the observe phase. apiCall, callInfo, err := builder.APICallBuilder(cli, clientInfo, apiaction.FindBy) if apiCall == nil { if !unstructuredtools.IsConditionSet(mg, condition.Creating()) && !unstructuredtools.IsConditionSet(mg, condition.Available()) { @@ -249,7 +254,7 @@ func (h *handler) Observe(ctx context.Context, mg *unstructured.Unstructured) (c if b != nil { err = populateStatusFields(clientInfo, mg, b) if err != nil { - log.Error(err, "Updating status fields (identifiers and additionalStatusFields)") + log.Error(err, "Populating status fields (identifiers and additionalStatusFields)") return controller.ExternalObservation{}, err } @@ -324,6 +329,7 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err return err } cli.Debug = meta.IsVerbose(mg) + cli.PrettyJSON = h.prettyJSONDebug cli.SetAuth = clientInfo.SetAuth apiCall, callInfo, err := builder.APICallBuilder(cli, clientInfo, apiaction.Create) @@ -342,6 +348,12 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err return err } + // Clear status before populating with new values to ensure no stale values remain. + // This prevents, for instance, using outdated identifiers (e.g., status.id from a previously deleted + // external resource) that would cause reconciliation deadlock on subsequent Observe operations. + clearStatusFields(mg) + log.Debug("Cleared status before populating with create response", "kind", mg.GetKind()) + if response.ResponseBody != nil { body := response.ResponseBody b, ok := body.(map[string]interface{}) @@ -352,12 +364,15 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err err = populateStatusFields(clientInfo, mg, b) if err != nil { - log.Error(err, "Updating identifiers") + log.Error(err, "Populating status fields (identifiers and additionalStatusFields)") return err } + } else { + log.Debug("Create response has no body, status will remain empty until discovered by next observe", "kind", mg.GetKind()) } log.Debug("Creating external resource", "kind", mg.GetKind()) + // Set condition for pending responses to indicate async creation operation in progress. if response.IsPending() { log.Debug("External resource is pending", "kind", mg.GetKind()) err = unstructuredtools.SetConditions(mg, customcondition.Pending()) @@ -365,7 +380,7 @@ func (h *handler) Create(ctx context.Context, mg *unstructured.Unstructured) err log.Error(err, "Setting condition") return err } - } else { + } else { // Set creating condition if not pending err = unstructuredtools.SetConditions(mg, condition.Creating()) if err != nil { log.Error(err, "Setting condition") @@ -409,6 +424,7 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err return err } cli.Debug = meta.IsVerbose(mg) + cli.PrettyJSON = h.prettyJSONDebug cli.SetAuth = clientInfo.SetAuth apiCall, callInfo, err := builder.APICallBuilder(cli, clientInfo, apiaction.Update) @@ -428,7 +444,13 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err return err } + // Clear status before populating with new values, unless the response has empty body. + // If response body is empty (e.g., 204 responses), we keep the existing status + // since the API indicates success without returning data (edge case). if response.ResponseBody != nil { + clearStatusFields(mg) + log.Debug("Cleared status before populating with update response", "kind", mg.GetKind()) + body := response.ResponseBody b, ok := body.(map[string]interface{}) if !ok { @@ -438,12 +460,30 @@ func (h *handler) Update(ctx context.Context, mg *unstructured.Unstructured) err err = populateStatusFields(clientInfo, mg, b) if err != nil { - log.Error(err, "Updating identifiers") + log.Error(err, "Populating status fields (identifiers and additionalStatusFields)") return err } + } else { + log.Debug("Update response has no body, keeping existing status", "kind", mg.GetKind()) } log.Debug("Updating external resource", "kind", mg.GetKind()) + // Set condition for pending responses to indicate async update operation in progress. + if response.IsPending() { + log.Debug("External resource update is pending", "kind", mg.GetKind()) + err = unstructuredtools.SetConditions(mg, customcondition.Pending()) + if err != nil { + log.Error(err, "Setting condition") + return err + } + } else { // Set creating condition if not pending (using Creating condition for updates as well since no Update condition exists) + err = unstructuredtools.SetConditions(mg, condition.Creating()) + if err != nil { + log.Error(err, "Setting condition") + return err + } + } + mg, err = tools.UpdateStatus(ctx, mg, tools.UpdateOptions{ Pluralizer: h.pluralizer, DynamicClient: h.dynamicClient, @@ -490,6 +530,7 @@ func (h *handler) Delete(ctx context.Context, mg *unstructured.Unstructured) err return err } cli.Debug = meta.IsVerbose(mg) + cli.PrettyJSON = h.prettyJSONDebug cli.SetAuth = clientInfo.SetAuth _, err = unstructuredtools.GetFieldsFromUnstructured(mg, "status") diff --git a/internal/controllers/restResources_test.go b/internal/controllers/restResources_test.go index a2947cf..fb879e4 100644 --- a/internal/controllers/restResources_test.go +++ b/internal/controllers/restResources_test.go @@ -29,8 +29,8 @@ import ( unstructuredtools "github.com/krateoplatformops/unstructured-runtime/pkg/tools/unstructured" "github.com/krateoplatformops/unstructured-runtime/pkg/tools/unstructured/condition" - "github.com/krateoplatformops/snowplow/plumbing/e2e" - xenv "github.com/krateoplatformops/snowplow/plumbing/env" + "github.com/krateoplatformops/plumbing/e2e" + xenv "github.com/krateoplatformops/plumbing/env" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -259,7 +259,7 @@ func TestController(t *testing.T) { log.Debug("Creating chart url info getter.", "error", err) } - handler = NewHandler(cfg.Client().RESTConfig(), log, swg, pluralizer) + handler = NewHandler(cfg.Client().RESTConfig(), log, swg, pluralizer, true) return ctx }). diff --git a/internal/tools/client/builder/builder.go b/internal/tools/client/builder/builder.go index d5548ba..64b6281 100644 --- a/internal/tools/client/builder/builder.go +++ b/internal/tools/client/builder/builder.go @@ -112,11 +112,10 @@ func BuildCallConfig(callInfo *CallInfo, mg *unstructured.Unstructured, configSp specFields, err := unstructuredtools.GetFieldsFromUnstructured(mg, "spec") if err != nil { - //log.Printf("Error retrieving spec fields from unstructured: %s\n", err) - //log.Printf("Initializing specFields as empty map\n") specFields = make(map[string]interface{}) // Initialize as empty map if error when retrieving spec } + // TODO: debug prints //log.Printf("Spec fields retrieved from unstructured:\n") //for k, v := range specFields { // log.Printf("Spec field key: %s, value: %v\n", k, v) @@ -124,8 +123,6 @@ func BuildCallConfig(callInfo *CallInfo, mg *unstructured.Unstructured, configSp statusFields, err := unstructuredtools.GetFieldsFromUnstructured(mg, "status") if err != nil { - //log.Printf("Error retrieving status fields from unstructured: %s\n", err) - //log.Printf("Initializing statusFields as empty map\n") statusFields = make(map[string]interface{}) // Initialize as empty map if error when retrieving status } @@ -138,8 +135,6 @@ func BuildCallConfig(callInfo *CallInfo, mg *unstructured.Unstructured, configSp // 5. Set the body in the request configuration reqConfiguration.Body = mapBody - //log.Printf("[BuildCallConfig] reqConfiguration: %v\n", reqConfiguration) - return reqConfiguration } @@ -155,25 +150,21 @@ func applyRequestFieldMapping(callInfo *CallInfo, mg *unstructured.Unstructured, if len(pathSegments) == 0 { continue } - //log.Printf("Path segments for InCustomResource %s: %v\n", mapping.InCustomResource, pathSegments) val, found, err := unstructured.NestedFieldNoCopy(mg.Object, pathSegments...) if err != nil || !found { continue } - //log.Printf("Value for InCustomResource %s: %v\n", mapping.InCustomResource, val) if mapping.InPath != "" { // Parse InPath with pathparsing to be consistent with dot notation handling inPathSegments, err := pathparsing.ParsePath(mapping.InPath) if err != nil || len(inPathSegments) == 0 { - //log.Printf("Error parsing InPath %s: %s\n", mapping.InPath, err) continue } // It should be a single segment for path parameters since path parameters are flat if len(inPathSegments) != 1 { - //log.Printf("InPath %s has multiple segments after parsing, expected a single segment for path parameters\n", mapping.InPath) continue } @@ -185,13 +176,11 @@ func applyRequestFieldMapping(callInfo *CallInfo, mg *unstructured.Unstructured, // Parse InQuery with pathparsing to be consistent with dot notation handling inQuerySegments, err := pathparsing.ParsePath(mapping.InQuery) if err != nil || len(inQuerySegments) == 0 { - //log.Printf("Error parsing InQuery %s: %s\n", mapping.InQuery, err) continue } // It should be a single segment for query parameters since query parameters are flat if len(inQuerySegments) != 1 { - //log.Printf("InQuery %s has multiple segments after parsing, expected a single segment for query parameters\n", mapping.InQuery) continue } @@ -203,10 +192,8 @@ func applyRequestFieldMapping(callInfo *CallInfo, mg *unstructured.Unstructured, // Parse InBody with pathparsing to be consistent with dot notation handling inBodySegments, err := pathparsing.ParsePath(mapping.InBody) if err != nil || len(inBodySegments) == 0 { - //log.Printf("Error parsing InBody %s: %s\n", mapping.InBody, err) continue } - //log.Printf("InBody segments: %v\n", inBodySegments) // Perform deep copy and type conversions (e.g., float64 to int64). // This is needed since we will set the value in the body map and therefore we need to ensure the types are correct. @@ -221,7 +208,6 @@ func applyRequestFieldMapping(callInfo *CallInfo, mg *unstructured.Unstructured, // Set the value in the body map at the correct nested path err = unstructured.SetNestedField(mapBody, convertedValue, inBodySegments...) if err != nil { - //log.Printf("Error setting body field %s to value %v: %s\n", mapping.InBody, convertedValue, err) continue } @@ -239,13 +225,12 @@ func applyConfigSpec(req *restclient.RequestConfiguration, configSpec map[string return } - // Internal helper to avoid repetition + // Internal helper process := func(key string, dest map[string]string) { if actionConfig, found, err := unstructured.NestedMap(configSpec, key, action); err == nil && found && actionConfig != nil { for k, v := range actionConfig { stringVal := fmt.Sprintf("%v", v) // Convert any type to string dest[k] = stringVal - //log.Printf("Setting %s field %s to value %v from configuration spec\n", key, k, v) } } } @@ -298,7 +283,6 @@ func processFields(callInfo *CallInfo, fields map[string]interface{}, reqConfigu if callInfo.ReqParams.Query.Contains(field) { if _, ok := reqConfiguration.Query[field]; !ok { // Avoid overwriting existing values reqConfiguration.Query[field] = fmt.Sprintf("%v", value) - //log.Printf("Setting query field %s to value %v from resource\n", field, value) } } @@ -309,7 +293,6 @@ func processFields(callInfo *CallInfo, fields map[string]interface{}, reqConfigu if callInfo.ReqParams.Body.Contains(field) { if mapBody[field] == nil { mapBody[field] = value - //log.Printf("Setting body field %s to value %v\n", field, value) } } } diff --git a/internal/tools/client/clienttools.go b/internal/tools/client/clienttools.go index b2c43b4..8a6151a 100644 --- a/internal/tools/client/clienttools.go +++ b/internal/tools/client/clienttools.go @@ -88,10 +88,11 @@ type UnstructuredClient struct { IdentifiersMatchPolicy string Resource *unstructured.Unstructured //Doc libopenapi.Document // Parsed OpenAPI document by libopenapi, needed for http request validation. TODO: to be re-enabled when libopenapi-validator is stable - DocScheme *libopenapi.DocumentModel[v3.Document] // OpenAPI document model (high-level) - Server string - Debug bool - SetAuth func(req *http.Request) + DocScheme *libopenapi.DocumentModel[v3.Document] // OpenAPI document model (high-level) + Server string + Debug bool + PrettyJSON bool + SetAuth func(req *http.Request) //Validator Validator // Validator for request validation. TODO: to be re-enabled when libopenapi-validator is stable } @@ -107,40 +108,38 @@ type RequestConfiguration struct { // isInResource is a method used during a "FindBy" operation. // It compares a value from an API response with the corresponding value in the local Unstructured resource. // It checks for the identifier's presence and correctness in 'spec' first, then falls back to checking 'status'. -// TODO: to be evaluated for potential addition of `ResponseFieldMapping` (possibly in future versions). +// TODO: to be re-evaluated and possibly modified for potential addition of `ResponseFieldMapping` (possibly in future versions). func (u *UnstructuredClient) isInResource(responseValue interface{}, fieldPath ...string) (bool, error) { if u.Resource == nil { return false, fmt.Errorf("resource is nil") } - // Check 1: Look for the identifier in the 'spec'. - if localValue, found, err := unstructured.NestedFieldNoCopy(u.Resource.Object, append([]string{"spec"}, fieldPath...)...); err == nil && found { - // If the field is found in the spec, we compare it. - // If it matches, we have a definitive match and can return true. - //log.Printf("isInResource - found in spec: localValue=%v, responseValue=%v", localValue, responseValue) - if comparison.DeepEqual(localValue, responseValue) { - //log.Print("isInResource - comparison DeepEqual returned true") - return true, nil - } - } else if err != nil { + // Check 1: Look for the identifier in 'spec'. + specPath := append([]string{"spec"}, fieldPath...) + localValue, found, err := unstructured.NestedFieldNoCopy(u.Resource.Object, specPath...) + if err != nil { return false, fmt.Errorf("error searching for identifier in spec: %w", err) } + // If the field is found in the spec, we compare it. + // If it matches, we have a definitive match and can return true. + if found && comparison.DeepEqual(localValue, responseValue) { + return true, nil + } - // Check 2: If the identifier was not found in spec, or if it was found but did not match, - // we proceed to check the 'status'. This is common for server-assigned identifiers. - // Last resort check, even if it makes less sense to search for findby identifiers in status. - if localValue, found, err := unstructured.NestedFieldNoCopy(u.Resource.Object, append([]string{"status"}, fieldPath...)...); err == nil && found { - // If found in status, we compare it. This is the last chance for a match. - //log.Printf("isInResource - found in status: localValue=%v, responseValue=%v", localValue, responseValue) - if comparison.DeepEqual(localValue, responseValue) { - //log.Print("isInResource - comparison DeepEqual returned true") - return true, nil - } - } else if err != nil { + // Check 2: If the identifier was not found in 'spec', or if it was found but did not match, + // we proceed to check the 'status'. This is common for server-assigned identifiers + // and not with identifiers normally used for a findby operation. + // Last resort check, even if it makes less sense to search for findby identifiers in 'status'. + statusPath := append([]string{"status"}, fieldPath...) + localValue, found, err = unstructured.NestedFieldNoCopy(u.Resource.Object, statusPath...) + if err != nil { return false, fmt.Errorf("error searching for identifier in status: %w", err) } + // If found in status, we compare it. This is the last chance for a match. + if found && comparison.DeepEqual(localValue, responseValue) { + return true, nil + } - //log.Printf("isInResource - identifier not found in spec or status for path %v", fieldPath) // No match. return false, nil } diff --git a/internal/tools/client/restclient.go b/internal/tools/client/restclient.go index d3dac57..f17c56c 100644 --- a/internal/tools/client/restclient.go +++ b/internal/tools/client/restclient.go @@ -108,8 +108,9 @@ func (u *UnstructuredClient) Call(ctx context.Context, cli *http.Client, path st if u.Debug { cli.Transport = &debuggingRoundTripper{ - Transport: cli.Transport, - Out: os.Stdout, + Transport: cli.Transport, + Out: os.Stdout, + PrettyJSON: u.PrettyJSON, } } @@ -199,18 +200,16 @@ func (u *UnstructuredClient) Call(ctx context.Context, cli *http.Client, path st func (u *UnstructuredClient) FindBy(ctx context.Context, cli *http.Client, path string, opts *RequestConfiguration, findByAction *getter.VerbsDescription) (Response, error) { if findByAction == nil || findByAction.Pagination == nil { // No pagination configured, perform a single call. - //log.Println("FindBy - no pagination configured, performing single call") return u.CallFindBySingle(ctx, cli, path, opts) } - //log.Println("FindBy - pagination configured, performing paginated calls") - // Set up debug transport once, before pagination starts if u.Debug { if _, ok := cli.Transport.(*debuggingRoundTripper); !ok { cli.Transport = &debuggingRoundTripper{ - Transport: cli.Transport, - Out: os.Stdout, + Transport: cli.Transport, + Out: os.Stdout, + PrettyJSON: u.PrettyJSON, } } } @@ -222,24 +221,17 @@ func (u *UnstructuredClient) FindBy(ctx context.Context, cli *http.Client, path } if paginator == nil { // Paginator factory returned nil, treat as no pagination. - //log.Println("FindBy - paginator is nil, not normal behavior, performing single call as fallback") return u.CallFindBySingle(ctx, cli, path, opts) } paginator.Init() - //counter := 0 - //log.Printf("FindBy - starting pagination loop with paginator type: %T", paginator) for { - //counter++ // Build and execute the request with the current paginator configuration (e.g., continuationToken). - //log.Printf("FindBy - pagination loop iteration %d", counter) - //log.Println("FindBy - executing paginated call") response, httpResp, err := u.CallForPagination(ctx, cli, path, opts, paginator) if err != nil { return Response{}, err } - //log.Printf("FindBy - received response for pagination iteration %d", counter) // Normalize the response to a list of items. itemList, err := u.extractItemsFromResponse(response.ResponseBody) @@ -251,7 +243,6 @@ func (u *UnstructuredClient) FindBy(ctx context.Context, cli *http.Client, path // Search for a matching item in the current page's results. if matchedItem, found := u.findItemInList(itemList); found { // Found a match, return it immediately. - //log.Printf("FindBy - found matching item on pagination iteration number: %d", counter) return Response{ ResponseBody: matchedItem, statusCode: response.statusCode, @@ -266,12 +257,10 @@ func (u *UnstructuredClient) FindBy(ctx context.Context, cli *http.Client, path } if !shouldContinue { - //log.Println("FindBy - pagination complete, no more pages to check") // Paginator says we are done, break the loop. break } } - //log.Println("FindBy - exited pagination loop without finding a match") // If the loop completes without finding a match, return a Not Found error. return Response{}, &StatusError{ @@ -528,26 +517,21 @@ func (u *UnstructuredClient) findItemInList(items []interface{}) (map[string]int // Default is "OR" if not specified. func (u *UnstructuredClient) isItemMatch(itemMap map[string]interface{}) (bool, error) { policy := strings.ToLower(u.IdentifiersMatchPolicy) - //log.Printf("isItemMatch - using IdentifiersMatchPolicy: %s", policy) if policy == "" || (policy != "and" && policy != "or") { policy = "or" // Default to "or" if not specified or invalid - //log.Printf("isItemMatch - defaulting IdentifiersMatchPolicy to: %s", policy) } // If no identifiers are specified, no match is possible. if len(u.IdentifierFields) == 0 { // TODO: probably warning or error log - //log.Print("isItemMatch - no IdentifierFields specified, cannot perform match\n") return false, nil } switch policy { case "and": - //log.Print("isItemMatch - AND logic\n") // AND Logic: Return false on the first failed match. for _, ide := range u.IdentifierFields { pathSegments, err := pathparsing.ParsePath(ide) - //log.Printf("Checking identifier: %s", ide) if err != nil || len(pathSegments) == 0 { continue } @@ -567,35 +551,26 @@ func (u *UnstructuredClient) isItemMatch(itemMap map[string]interface{}) (bool, // If any identifier does not match, it's not an AND match. return false, nil } - //log.Printf("isItemMatch - identifier %s matched", ide) } // If the loop completes, it means all identifiers matched (AND logic succeeded). - //log.Print("isItemMatch - AND logic succeeded, all identifiers matched\n") return true, nil case "or": - //log.Print("isItemMatch - using OR logic for identifier matching\n") // OR Logic (default): Return true on the first successful identifier match. for _, ide := range u.IdentifierFields { - //log.Print("isItemMatch - OR logic\n") - //log.Printf("Checking identifier: %s", ide) pathSegments, err := pathparsing.ParsePath(ide) - //log.Printf("Parsed path segments: %v", pathSegments) if err != nil || len(pathSegments) == 0 { continue } val, found, err := unstructured.NestedFieldNoCopy(itemMap, pathSegments...) - //log.Printf("isItemMatch - checking identifier %s: value=%v, found=%v, err=%v", ide, val, found, err) - //log.Print("isItemMatch, after successful check\n") if err != nil || !found { // If field is not found or there is an error, it's not a match for this identifier, so we continue. continue } ok, err := u.isInResource(val, pathSegments...) - //log.Printf("isItemMatch - comparison result for identifier %s: ok=%v, err=%v", ide, ok, err) if err != nil { // A hard error during comparison should be propagated up. // TODO: is this the desired behavior for OR logic? return false, err @@ -607,11 +582,9 @@ func (u *UnstructuredClient) isItemMatch(itemMap map[string]interface{}) (bool, } } - //log.Print("isItemMatch - no identifiers matched\n") // If the loop completes, no identifiers matched (OR logic failed). return false, nil default: - //log.Printf("isItemMatch - unknown IdentifiersMatchPolicy: %s", policy) return false, fmt.Errorf("unknown identifier match policy: %s", u.IdentifiersMatchPolicy) } } @@ -659,8 +632,9 @@ func handleResponse(rc io.ReadCloser, response any) error { } type debuggingRoundTripper struct { - Transport http.RoundTripper - Out io.Writer + Transport http.RoundTripper + Out io.Writer + PrettyJSON bool } func (d *debuggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) { @@ -681,14 +655,99 @@ func (d *debuggingRoundTripper) RoundTrip(req *http.Request) (*http.Response, er return resp, err } - b, err = httputil.DumpResponse(resp, req.URL.Query().Get("watch") != "true") + // Dump the response: use pretty JSON if enabled, otherwise use httputil.DumpResponse + if d.PrettyJSON { + err = d.dumpResponseWithPrettyJSON(resp, req.URL.Query().Get("watch") != "true") + if err != nil { + return nil, err + } + } else { + b, err := httputil.DumpResponse(resp, req.URL.Query().Get("watch") != "true") + if err != nil { + return nil, err + } + d.Out.Write(b) + d.Out.Write([]byte{'\n'}) + } + + return resp, nil +} + +// dumpResponseWithPrettyJSON dumps the HTTP response with pretty-printed JSON body if applicable +func (d *debuggingRoundTripper) dumpResponseWithPrettyJSON(resp *http.Response, dumpBody bool) error { + if resp == nil { + return fmt.Errorf("response is nil") + } + + // Dump status line and headers first + var b bytes.Buffer + fmt.Fprintf(&b, "%s %s\r\n", resp.Proto, resp.Status) + resp.Header.Write(&b) + b.WriteString("\r\n") + + if !dumpBody || resp.Body == nil { + // No body to dump + d.Out.Write(b.Bytes()) + d.Out.Write([]byte{'\n'}) + return nil + } + + // Read the body + bodyBytes, err := io.ReadAll(resp.Body) if err != nil { - return nil, err + return fmt.Errorf("reading response body: %w", err) } - d.Out.Write(b) + + // Close the original body and replace it with a new reader so the response can still be read + resp.Body.Close() + resp.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + // Check if the body is JSON and pretty-print it + prettyBody, isJSON := tryPrettyPrintJSON(bodyBytes) + if isJSON { + b.Write(prettyBody) + } else { // Fallback + // Not JSON or invalid JSON, write as-is + b.Write(bodyBytes) + } + + b.WriteString("\r\n") + d.Out.Write(b.Bytes()) d.Out.Write([]byte{'\n'}) - return resp, err + return nil +} + +// tryPrettyPrintJSON attempts to pretty-print JSON with colors. Returns the formatted bytes and true if successful. +func tryPrettyPrintJSON(data []byte) ([]byte, bool) { + // Quick check: if empty or doesn't start with { or [, it's not JSON + trimmed := bytes.TrimSpace(data) + if len(trimmed) == 0 { + // Empty body + return data, false + } + + firstChar := trimmed[0] + if firstChar != '{' && firstChar != '[' { + // Not JSON + return data, false + } + + // Try to unmarshal to verify it's valid JSON + var jsonData interface{} + if err := json.Unmarshal(data, &jsonData); err != nil { + // Not valid JSON + return data, false + } + + // Pretty-print with 2-space indentation + prettyJSON, err := json.MarshalIndent(jsonData, "", " ") + if err != nil { + // Marshaling failed (shouldn't happen if unmarshal succeeded, but be safe) + return data, false + } + + return prettyJSON, true } // TODO: to be re-enabled when libopenapi-validator is stable diff --git a/internal/tools/comparison/comparison.go b/internal/tools/comparison/comparison.go index 8440d11..0d9dfba 100644 --- a/internal/tools/comparison/comparison.go +++ b/internal/tools/comparison/comparison.go @@ -43,7 +43,6 @@ func CompareExisting(mg map[string]interface{}, rm map[string]interface{}, path for key, value := range mg { currentPath := append(path, key) pathStr := fmt.Sprintf("%v", currentPath) - //log.Printf("Comparing field at path: %s", pathStr) rmValue, ok := rm[key] if !ok { @@ -51,14 +50,12 @@ func CompareExisting(mg map[string]interface{}, rm map[string]interface{}, path // TODO: to be understood if this is the desired behavior // Examples: // Key [configurationRef] not found in rm, ignoring and continuing (this is desired, but maybe can be whitelisted) - //log.Printf("Key %s not found in rm, ignoring and continuing", pathStr) continue } // Handle case where one or both values are nil if value == nil || rmValue == nil { if value == nil && rmValue == nil { - //log.Printf("Both values are nil at %s, considered equal for this field", pathStr) continue // Both are nil, considered equal } // One is nil but the other isn't, so they are not equal. @@ -211,7 +208,6 @@ func CompareExisting(mg map[string]interface{}, rm map[string]interface{}, path } default: // Here we compare primary types (string, bool, numbers) - //log.Printf("Arrived at default case for key %s with local value '%v' and remote value '%v'", pathStr, value, rmValue) ok := CompareAny(value, rmValue) if !ok { return ComparisonResult{ @@ -230,8 +226,6 @@ func CompareExisting(mg map[string]interface{}, rm map[string]interface{}, path } func CompareAny(a any, b any) bool { - //log.Printf("CompareAny - Initial values: '%v' and '%v'\n", a, b) - if a == nil && b == nil { return true } @@ -241,11 +235,9 @@ func CompareAny(a any, b any) bool { strA := fmt.Sprintf("%v", a) strB := fmt.Sprintf("%v", b) - //log.Printf("Comparing values: '%s' and '%s'\n", strA, strB) a = InferType(strA) b = InferType(strB) - //log.Printf("Normalized values: '%v' and '%v'\n", a, b) //log.Printf("Values to compare: '%v' and '%v'\n", a, b) //diff := cmp.Diff(a, b) @@ -261,8 +253,6 @@ func CompareAny(a any, b any) bool { // For slices (arrays), element order and content are strictly compared. // Map and slice comparisons normalize nil values before comparison to avoid discrepancies due to nil entries. func DeepEqual(a, b interface{}) bool { - //log.Printf("DeepEqual - Values to compare: '%v' and '%v'\n", a, b) - if a == nil && b == nil { return true } @@ -272,7 +262,6 @@ func DeepEqual(a, b interface{}) bool { aKind := reflect.TypeOf(a).Kind() bKind := reflect.TypeOf(b).Kind() - //log.Printf("Types of values: aKind=%v, bKind=%v\n", aKind, bKind) // For complex types, a direct recursive comparison is correct and respects // the nuances of map and slice comparison. if aKind == reflect.Map || aKind == reflect.Slice || bKind == reflect.Map || bKind == reflect.Slice { @@ -297,7 +286,6 @@ func DeepEqual(a, b interface{}) bool { normB := InferType(strB) // DEBUG - //log.Print("Inside DeepEqual function, after normalization:") //log.Printf("Comparing normalized values: '%v' and '%v'\n", normA, normB) //diff := cmp.Diff(normA, normB) //log.Printf("cmp diff:\n%s", diff) diff --git a/internal/tools/pagination/continuation_token.go b/internal/tools/pagination/continuation_token.go index e61887f..990de71 100644 --- a/internal/tools/pagination/continuation_token.go +++ b/internal/tools/pagination/continuation_token.go @@ -30,11 +30,8 @@ func (p *continuationTokenPaginator) Init() { // UpdateRequest adds the pagination token to the http.Request. func (p *continuationTokenPaginator) UpdateRequest(req *http.Request) error { - //log.Print("UpdateRequest") // Don't add a token on the very first call or if the token is empty. if p.isFirstCall || p.nextToken == "" { - //log.Print("Skipping token addition on first call or empty token") - //log.Printf("isFirstCall: %v, nextToken: '%s'", p.isFirstCall, p.nextToken) p.isFirstCall = false return nil } @@ -45,7 +42,6 @@ func (p *continuationTokenPaginator) UpdateRequest(req *http.Request) error { q := req.URL.Query() q.Set(cfg.TokenPath, p.nextToken) req.URL.RawQuery = q.Encode() - //log.Printf("Added continuation token to query param '%s': %s", cfg.TokenPath, p.nextToken) case "header": req.Header.Set(cfg.TokenPath, p.nextToken) default: @@ -75,13 +71,11 @@ func (p *continuationTokenPaginator) ShouldContinue(resp *http.Response, body [] // If a new token is found and it's not empty, we should continue. if extractedToken != "" { - //log.Printf("Continuation Token found '%s': %s", cfg.TokenPath, extractedToken) p.nextToken = extractedToken return true, nil } // No more tokens, we're done. - //log.Printf("No Continuation Token found, ending pagination.") p.nextToken = "" return false, nil } diff --git a/internal/tools/pagination/paginator.go b/internal/tools/pagination/paginator.go index ff34f8d..2a3c68a 100644 --- a/internal/tools/pagination/paginator.go +++ b/internal/tools/pagination/paginator.go @@ -23,13 +23,11 @@ type Paginator interface { // NewPaginator is a factory that returns the correct paginator based on config. func NewPaginator(config *getter.Pagination) (Paginator, error) { if config == nil { - //log.Printf("NewPaginator: no pagination config provided") return nil, nil // No pagination configured } switch config.Type { case "continuationToken": - //log.Printf("NewPaginator: creating continuationToken paginator") // Ensure that the ContinuationToken config is not nil to avoid panics. if config.ContinuationToken == nil { return nil, fmt.Errorf("pagination type is 'continuationToken' but the continuationToken config block is missing") diff --git a/main.go b/main.go index 8387f4b..2469d78 100644 --- a/main.go +++ b/main.go @@ -11,9 +11,9 @@ import ( "time" "github.com/go-logr/logr" + "github.com/krateoplatformops/plumbing/env" "github.com/krateoplatformops/plumbing/ptr" prettylog "github.com/krateoplatformops/plumbing/slogs/pretty" - "github.com/krateoplatformops/snowplow/plumbing/env" "github.com/krateoplatformops/unstructured-runtime/pkg/controller/builder" "github.com/krateoplatformops/unstructured-runtime/pkg/logging" "github.com/krateoplatformops/unstructured-runtime/pkg/metrics/server" @@ -38,29 +38,45 @@ var ( func main() { // Flags - kubeconfig := flag.String("kubeconfig", env.String("KUBECONFIG", ""), + kubeconfig := flag.String("kubeconfig", + env.String("KUBECONFIG", ""), "absolute path to the kubeconfig file") debug := flag.Bool("debug", - env.Bool("REST_CONTROLLER_DEBUG", false), "dump verbose output") - workers := flag.Int("workers", env.Int("REST_CONTROLLER_WORKERS", 5), "number of workers") + env.Bool("REST_CONTROLLER_DEBUG", false), + "dump verbose output") + prettyJSONDebug := flag.Bool("pretty-json-debug", + env.Bool("REST_CONTROLLER_PRETTY_JSON_DEBUG", false), + "globally enable pretty-print JSON formatting in HTTP debug output (response bodies)") + workers := flag.Int("workers", + env.Int("REST_CONTROLLER_WORKERS", 5), + "number of workers") resyncInterval := flag.Duration("resync-interval", - env.Duration("REST_CONTROLLER_RESYNC_INTERVAL", time.Minute*3), "resync interval") + env.Duration("REST_CONTROLLER_RESYNC_INTERVAL", time.Minute*3), + "resync interval") resourceGroup := flag.String("group", - env.String("REST_CONTROLLER_GROUP", ""), "resource api group") + env.String("REST_CONTROLLER_GROUP", ""), + "resource api group") resourceVersion := flag.String("version", - env.String("REST_CONTROLLER_VERSION", ""), "resource api version") + env.String("REST_CONTROLLER_VERSION", ""), + "resource api version") resourceName := flag.String("resource", - env.String("REST_CONTROLLER_RESOURCE", ""), "resource plural name") + env.String("REST_CONTROLLER_RESOURCE", ""), + "resource plural name") namespace := flag.String("namespace", - env.String("REST_CONTROLLER_NAMESPACE", ""), "namespace to watch, empty for all namespaces") + env.String("REST_CONTROLLER_NAMESPACE", ""), + "namespace to watch, empty for all namespaces") maxErrorRetryInterval := flag.Duration("max-error-retry-interval", - env.Duration("REST_CONTROLLER_MAX_ERROR_RETRY_INTERVAL", 90*time.Second), "The maximum interval between retries when an error occurs. This should be less than the half of the resync interval.") + env.Duration("REST_CONTROLLER_MAX_ERROR_RETRY_INTERVAL", 90*time.Second), + "The maximum interval between retries when an error occurs. This should be less than the half of the resync interval.") minErrorRetryInterval := flag.Duration("min-error-retry-interval", - env.Duration("REST_CONTROLLER_MIN_ERROR_RETRY_INTERVAL", 1*time.Second), "The minimum interval between retries when an error occurs. This should be less than max-error-retry-interval.") + env.Duration("REST_CONTROLLER_MIN_ERROR_RETRY_INTERVAL", 1*time.Second), + "The minimum interval between retries when an error occurs. This should be less than max-error-retry-interval.") maxErrorRetry := flag.Int("max-error-retries", - env.Int("REST_CONTROLLER_MAX_ERROR_RETRIES", 5), "How many times to retry the processing of a resource when an error occurs before giving up and dropping the resource.") + env.Int("REST_CONTROLLER_MAX_ERROR_RETRIES", 5), + "How many times to retry the processing of a resource when an error occurs before giving up and dropping the resource.") metricsServerPort := flag.Int("metrics-server-port", - env.Int("REST_CONTROLLER_METRICS_SERVER_PORT", 0), "The address to bind the metrics server to. If empty, metrics server is disabled.") + env.Int("REST_CONTROLLER_METRICS_SERVER_PORT", 0), + "The address to bind the metrics server to. If empty, metrics server is disabled.") flag.Usage = func() { fmt.Fprintln(flag.CommandLine.Output(), "Flags:") @@ -110,6 +126,7 @@ func main() { log.WithValues("build", Build). WithValues("debug", *debug). + WithValues("prettyJSONDebug", *prettyJSONDebug). WithValues("resyncInterval", *resyncInterval). WithValues("group", *resourceGroup). WithValues("version", *resourceVersion). @@ -121,7 +138,7 @@ func main() { WithValues("workers", *workers). Info("Starting.", "serviceName", serviceName) - handler = restResources.NewHandler(cfg, log, swg, *pluralizer) + handler = restResources.NewHandler(cfg, log, swg, *pluralizer, *prettyJSONDebug) if handler == nil { log.Error(fmt.Errorf("handler is nil"), "Creating handler for controller.") os.Exit(1)