diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml index ed8bc09..04b1aee 100644 --- a/.github/workflows/code-quality.yml +++ b/.github/workflows/code-quality.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [ main ] +permissions: + contents: read + jobs: code-quality: name: Code Quality Checks @@ -16,14 +19,15 @@ jobs: uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: 'go.mod' cache: true - - name: Install golangci-lint - run: | - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0 + - name: golangci-lint + uses: golangci/golangci-lint-action@v9 + with: + version: v2.12.2 - name: Check formatting run: | diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 2fa98a5..16042b4 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -14,6 +14,7 @@ env: jobs: docker-build-push: + if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository name: Build and Push Multi-arch Image runs-on: ubuntu-latest permissions: diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 8da1082..126016b 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -29,6 +29,7 @@ jobs: fi create-gke-cluster: + if: github.event.pull_request.head.repo.full_name == github.repository uses: ./.github/workflows/create-dev-cluster.yml with: cluster-name: infra-roxie-pr-${{ github.event.pull_request.number }}-gke @@ -38,7 +39,7 @@ jobs: create-openshift-cluster: needs: check-olm-label - if: needs.check-olm-label.outputs.has-label == 'true' + if: needs.check-olm-label.outputs.has-label == 'true' && github.event.pull_request.head.repo.full_name == github.repository uses: ./.github/workflows/create-dev-cluster.yml with: cluster-name: infra-roxie-pr-${{ github.event.pull_request.number }}-openshift @@ -47,6 +48,7 @@ jobs: secrets: inherit build-roxie-image: + if: github.event.pull_request.head.repo.full_name == github.repository uses: ./.github/workflows/build-roxie-image.yml permissions: contents: read @@ -54,6 +56,7 @@ jobs: secrets: inherit e2e-tests: + if: github.event.pull_request.head.repo.full_name == github.repository needs: [ create-gke-cluster, build-roxie-image ] uses: ./.github/workflows/e2e-tests.yml with: @@ -62,6 +65,7 @@ jobs: secrets: inherit e2e-tests-openshift: + if: github.event.pull_request.head.repo.full_name == github.repository needs: [ create-openshift-cluster, build-roxie-image ] uses: ./.github/workflows/e2e-tests.yml with: @@ -72,7 +76,7 @@ jobs: secrets: inherit delete-gke-cluster: - if: ${{ always() && needs.create-gke-cluster.result == 'success' }} + if: ${{ always() && needs.create-gke-cluster.result == 'success' && github.event.pull_request.head.repo.full_name == github.repository }} needs: [ create-gke-cluster, e2e-tests ] uses: ./.github/workflows/delete-dev-cluster.yml with: @@ -80,7 +84,7 @@ jobs: secrets: inherit delete-openshift-cluster: - if: ${{ always() && needs.create-openshift-cluster.result == 'success' }} + if: ${{ always() && needs.create-openshift-cluster.result == 'success' && github.event.pull_request.head.repo.full_name == github.repository }} needs: [ create-openshift-cluster, e2e-tests-openshift ] uses: ./.github/workflows/delete-dev-cluster.yml with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 31a9298..80ac08e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -5,13 +5,12 @@ on: tags: - 'v[0-9]+.[0-9]+.[0-9]+*' -permissions: - contents: write - jobs: build-and-release: name: Build and Release Binaries runs-on: ubuntu-latest + permissions: + contents: write steps: - name: Checkout code @@ -54,7 +53,7 @@ jobs: sha256sum roxie-* > checksums.txt - name: Create GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: name: Release ${{ steps.version.outputs.version }} draft: false diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index a3be2ce..3526570 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -3,6 +3,9 @@ name: Unit Tests on: workflow_call: +permissions: + contents: read + jobs: unit-tests: runs-on: ubuntu-latest diff --git a/.golangci.yml b/.golangci.yml index dbab888..8330b89 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,6 +1,6 @@ # golangci-lint configuration for roxie -version: 2 +version: "2" run: timeout: 5m @@ -15,6 +15,10 @@ linters: - ineffassign # Detect ineffectual assignments - misspell # Find commonly misspelled words + settings: + staticcheck: + checks: ["all", "-SA4009", "-ST1000", "-ST1003", "-ST1020"] + # Disable noisy linters disable: - errcheck # Too many false positives for cleanup code @@ -22,16 +26,12 @@ linters: - revive # Style linter is too opinionated - unparam # Often flags valid parameter flexibility -linters-settings: - staticcheck: - checks: ["all", "-SA4009"] # Disable "argument is overwritten" check - issues: # Don't limit the number of issues max-issues-per-linter: 0 max-same-issues: 0 output: - print-issued-lines: true - print-linter-name: true - sort-results: true + formats: + text: + path: stdout diff --git a/Dockerfile b/Dockerfile index 2c40af0..a26c41c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,7 @@ ARG TARGETARCH WORKDIR /build USER root +ENV GOTOOLCHAIN=auto # Copy go mod files first for better layer caching COPY go.mod go.sum ./ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index fd2b6a3..fd89a84 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,6 @@ +**PLEASE NOTE: This repository contains a deployment tool for ACS, which is used by +ACS engineers. It is **not** a general-purpose installation frontend for ACS or StackRox users.** + # roxie – ACS deployments made easy [![Code Quality](https://github.com/stackrox/roxie/actions/workflows/code-quality.yml/badge.svg)](https://github.com/stackrox/roxie/actions/workflows/code-quality.yml) diff --git a/cmd/deploy.go b/cmd/deploy.go index 87d7caa..dc5c7c2 100644 --- a/cmd/deploy.go +++ b/cmd/deploy.go @@ -347,11 +347,15 @@ func runDeploy(cmd *cobra.Command, args []string) error { } func configureConfig(log *logger.Logger, components component.Component, deploySettings *deployer.Config) error { - clusterType := env.GetCurrentClusterType() - log.Dimf("Detected cluster type: %v", clusterType) - defaults, err := clusterdefaults.ApplyClusterDefaults(clusterType, deploySettings) + if deploySettings.Roxie.ClusterType == types.ClusterTypeUnknown { + clusterType := env.GetCurrentClusterType() + log.Dimf("Detected cluster type: %v", clusterType) + deploySettings.Roxie.ClusterType = clusterType + } + clusterType := deploySettings.Roxie.ClusterType + defaults, err := clusterdefaults.ApplyClusterDefaults(deploySettings) if err != nil { - return fmt.Errorf("applying defaults for cluster type %v: %w", clusterType, err) + return err } if verbose { log.Dimf("Applying the following defaults based on detected cluster type %v:", clusterType) @@ -409,6 +413,8 @@ func deployValidate(components component.Component, deploySettings *deployer.Con return errors.New("running without a controlling terminal requires --envrc to be set") } + clusterType := deploySettings.Roxie.ClusterType + if env.RunningInRoxieContainer { // For running containerized we have specific requirements. if deploySettings.Central.PortForwardingEnabled() { @@ -419,7 +425,7 @@ func deployValidate(components component.Component, deploySettings *deployer.Con } // On infra OpenShift we already get image pull secrets for Quay automatically. - if clusterType := env.GetCurrentClusterType(); clusterType != types.ClusterTypeInfraOpenShift4 { + if clusterType != types.ClusterTypeInfraOpenShift4 { if os.Getenv("REGISTRY_USERNAME") == "" || os.Getenv("REGISTRY_PASSWORD") == "" { return fmt.Errorf("containerized mode requires REGISTRY_USERNAME and REGISTRY_PASSWORD environment variables for clusters of type %s", clusterType) } @@ -437,7 +443,6 @@ func deployValidate(components component.Component, deploySettings *deployer.Con if deploySettings.Operator.DeployViaOlm { return errors.New("using Konflux images while deploying operator via OLM is not supported") } - clusterType := env.GetCurrentClusterType() if !clusterType.IsOpenShift() { return fmt.Errorf("--konflux flag is only supported on OpenShift 4 clusters (current cluster type: %s)", clusterType.String()) } diff --git a/go.mod b/go.mod index 03e0c77..63116be 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/stackrox/roxie -go 1.25.6 +go 1.26.0 require ( dario.cat/mergo v1.0.2 @@ -9,16 +9,36 @@ require ( github.com/spf13/cobra v1.10.2 github.com/spf13/pflag v1.0.10 github.com/stretchr/testify v1.11.1 - golang.org/x/term v0.38.0 + golang.org/x/term v0.39.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/apimachinery v0.35.3 - k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 + k8s.io/api v0.36.1 + k8s.io/apimachinery v0.36.1 + k8s.io/client-go v0.36.1 + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 +) + +require ( + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/josharian/intern v1.0.0 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect ) require ( github.com/Masterminds/semver/v3 v3.4.0 github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/docker/cli v29.4.0+incompatible // indirect github.com/docker/docker-credential-helpers v0.9.3 // indirect github.com/fxamacker/cbor/v2 v2.9.0 // indirect @@ -33,20 +53,20 @@ require ( github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sirupsen/logrus v1.9.4 // indirect github.com/vbatts/tar-split v0.12.2 // indirect github.com/x448/float16 v0.8.4 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - golang.org/x/net v0.48.0 // indirect + golang.org/x/net v0.49.0 // indirect golang.org/x/sync v0.20.0 // indirect golang.org/x/sys v0.43.0 // indirect golang.org/x/text v0.35.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gotest.tools/v3 v3.5.2 // indirect - k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) diff --git a/go.sum b/go.sum index 9c9eb73..3f3dd31 100644 --- a/go.sum +++ b/go.sum @@ -5,30 +5,57 @@ github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lpr github.com/containerd/stargz-snapshotter/estargz v0.18.2 h1:yXkZFYIzz3eoLwlTUZKz2iQ4MrckBxJjkmD16ynUTrw= github.com/containerd/stargz-snapshotter/estargz v0.18.2/go.mod h1:XyVU5tcJ3PRpkA9XS2T5us6Eg35yM0214Y+wvrZTBrY= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 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/docker/cli v29.4.0+incompatible h1:+IjXULMetlvWJiuSI0Nbor36lcJ5BTcVpUmB21KBoVM= github.com/docker/cli v29.4.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= +github.com/emicklei/go-restful/v3 v3.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 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-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +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/go-containerregistry v0.21.5 h1:KTJG9Pn/jC0VdZR6ctV3/jcN+q6/Iqlx0sTVz3ywZlM= github.com/google/go-containerregistry v0.21.5/go.mod h1:ySvMuiWg+dOsRW0Hw8GYwfMwBlNRTmpYBFJPlkco5zU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +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= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/klauspost/compress v1.18.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE= github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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= @@ -42,12 +69,17 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ 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/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= @@ -57,7 +89,14 @@ github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/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= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/vbatts/tar-split v0.12.2 h1:w/Y6tjxpeiFMR47yzZPlPj/FcPLpXbTUi/9H7d3CPa4= @@ -66,40 +105,55 @@ github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= +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/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= +golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.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/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= gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= -k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= -k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= -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-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= -k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= -k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= +k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= +k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= +k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= +k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/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/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/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/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/clusterdefaults/clusterdefaults.go b/internal/clusterdefaults/clusterdefaults.go index 588c8e9..739a981 100644 --- a/internal/clusterdefaults/clusterdefaults.go +++ b/internal/clusterdefaults/clusterdefaults.go @@ -13,12 +13,12 @@ import ( // provided deployer.Config. // Returns *just* the assembled defaults for the given cluster type for logging purposes. func ApplyClusterDefaults( - clusterType types.ClusterType, config *deployer.Config, ) (*deployer.Config, error) { if config == nil { panic("applying cluster defaults to nil config") } + clusterType := config.Roxie.ClusterType defaults := getDefaultsForClusterType(clusterType) if defaults == nil { return nil, nil @@ -27,11 +27,11 @@ func ApplyClusterDefaults( // Make a copy. defaultsCopy, err := defaults.DeepCopy() if err != nil { - return nil, fmt.Errorf("deep-copying cluster defaults: %w", err) + return nil, fmt.Errorf("deep-copying cluster defaults for cluster type %s: %w", clusterType, err) } if err := mergo.Merge(config, defaultsCopy, mergo.WithoutDereference); err != nil { - return nil, fmt.Errorf("merging-in cluster defaults: %w", err) + return nil, fmt.Errorf("merging-in cluster defaults for cluster type %s: %w", clusterType, err) } return defaultsCopy, nil diff --git a/internal/clusterdefaults/clusterdefaults_test.go b/internal/clusterdefaults/clusterdefaults_test.go index f0dab56..78b59e1 100644 --- a/internal/clusterdefaults/clusterdefaults_test.go +++ b/internal/clusterdefaults/clusterdefaults_test.go @@ -113,7 +113,8 @@ func TestClusterDefaults(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { config := tt.config - _, err := ApplyClusterDefaults(tt.clusterType, &config) + config.Roxie.ClusterType = tt.clusterType + _, err := ApplyClusterDefaults(&config) require.NoError(t, err) if tt.wantConfig.Central.Exposure == nil { diff --git a/internal/deployer/config.go b/internal/deployer/config.go index 907fc4b..b4fbeab 100644 --- a/internal/deployer/config.go +++ b/internal/deployer/config.go @@ -44,9 +44,10 @@ func (c *Config) DeepCopy() (*Config, error) { // RoxieConfig holds roxie-level settings such as version and feature flags. type RoxieConfig struct { - Version string `yaml:"version,omitempty"` - KonfluxImages bool `yaml:"konfluxImages,omitempty"` - FeatureFlags map[string]bool `yaml:"featureFlags,omitempty"` + Version string `yaml:"version,omitempty"` + KonfluxImages bool `yaml:"konfluxImages,omitempty"` + FeatureFlags map[string]bool `yaml:"featureFlags,omitempty"` + ClusterType types.ClusterType `yaml:"clusterType,omitempty"` } // NewRoxieConfig returns a RoxieConfig with initialized defaults. diff --git a/internal/deployer/deploy_via_operator.go b/internal/deployer/deploy_via_operator.go index f97a923..ed5bae2 100644 --- a/internal/deployer/deploy_via_operator.go +++ b/internal/deployer/deploy_via_operator.go @@ -119,7 +119,7 @@ func (d *Deployer) ensureOperatorDeployed(ctx context.Context) error { func (d *Deployer) deployCentralOperator(ctx context.Context) error { d.logger.Info("🚀 Deploying Central via Operator...") - needPullSecrets := env.GetCurrentClusterType() != types.ClusterTypeInfraOpenShift4 + needPullSecrets := d.config.Roxie.ClusterType != types.ClusterTypeInfraOpenShift4 if err := d.prepareNamespace(ctx, d.config.Central.Namespace, needPullSecrets); err != nil { return fmt.Errorf("failed to prepare namespace: %w", err) } @@ -655,7 +655,7 @@ func (d *Deployer) configureCentralEndpoint(ctx context.Context) error { func (d *Deployer) deploySecuredClusterOperator(ctx context.Context) error { d.logger.Info("🚀 Deploying SecuredCluster via Operator...") - needPullSecrets := env.GetCurrentClusterType() != types.ClusterTypeInfraOpenShift4 + needPullSecrets := d.config.Roxie.ClusterType != types.ClusterTypeInfraOpenShift4 if err := d.prepareNamespace(ctx, d.config.SecuredCluster.Namespace, needPullSecrets); err != nil { return fmt.Errorf("failed to prepare namespace: %w", err) } diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 9aec79d..95a320e 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -13,6 +13,7 @@ import ( "time" "github.com/fatih/color" + "k8s.io/client-go/kubernetes" "github.com/stackrox/roxie/internal/component" "github.com/stackrox/roxie/internal/dockerauth" @@ -46,6 +47,7 @@ type Deployer struct { envrcFile string kubeContext string + k8sClient kubernetes.Interface config Config @@ -263,6 +265,17 @@ func New(log *logger.Logger) (*Deployer, error) { d.kubeContext = env.GetCurrentContext() + // Created eagerly (not lazily on first use) because + // 1. we expect to make more extensive use of it + // 2. we need a working connection to the API server anyway. + if d.kubeContext != "" { + client, err := k8s.NewClient(d.kubeContext) + if err != nil { + return nil, fmt.Errorf("creating new Kubernetes client: %w", err) + } + d.k8sClient = client + } + log.Success("🚀 ACS Deployer initialized") return d, nil @@ -298,7 +311,7 @@ func (d *Deployer) stopDetachedPortForward() { // Deploy deploys the specified components to the cluster. func (d *Deployer) Deploy(ctx context.Context, components component.Component) error { // Prepare and verify credentials early to fail fast. - if env.GetCurrentClusterType() != types.ClusterTypeInfraOpenShift4 { + if d.config.Roxie.ClusterType != types.ClusterTypeInfraOpenShift4 { if err := d.prepareCredentials(); err != nil { return fmt.Errorf("failed to prepare credentials: %w", err) } @@ -306,6 +319,17 @@ func (d *Deployer) Deploy(ctx context.Context, components component.Component) e d.logger.Infof("Initiating deployment of %s", components) + if d.config.Roxie.KonfluxImages && d.config.Roxie.ClusterType == types.ClusterTypeOpenShift4 { + // For deploying Konflux-built images, we need to configure image-rewriting on the cluster at the CRI-O level. + // But due to https://access.redhat.com/solutions/6540591 the standard pull-secret mechanism doesn't work for the + // target image references. A workaround is to inject the pull secrets we need into OpenShift's global + // pull secrets. + // Infra OpenShift4 clusters already come equipped with this global pull secret. + if err := d.InjectGlobalOpenShiftPullSecret(ctx); err != nil { + return fmt.Errorf("injecting global OpenShift pull-secret for Konflux images: %w", err) + } + } + // If only deploying operator, use the operator-only flow. if components.IncludesOperatorExplicitly() { return d.deployOperatorOnly(ctx) @@ -778,7 +802,7 @@ func (d *Deployer) PrintCentralDeploymentSummary() { // Deployment details log.Info(cyan.Sprint("│") + createRow("Component", component)) - log.Info(cyan.Sprint("│") + createRow("Cluster Type", env.GetCurrentClusterType().String())) + log.Info(cyan.Sprint("│") + createRow("Cluster Type", d.config.Roxie.ClusterType.String())) log.Info(cyan.Sprint("│") + createRow("Main Tag", mainImageTag)) log.Info(cyan.Sprint("│") + createRow("Kubernetes Context", kubeContext)) @@ -957,7 +981,7 @@ func (d *Deployer) PrintSecuredClusterDeploymentSummary() { // Deployment details log.Info(cyan.Sprint("│") + createRow("Component", component)) - log.Info(cyan.Sprint("│") + createRow("Cluster Type", env.GetCurrentClusterType().String())) + log.Info(cyan.Sprint("│") + createRow("Cluster Type", d.config.Roxie.ClusterType.String())) log.Info(cyan.Sprint("│") + createRow("Main Tag", mainImageTag)) log.Info(cyan.Sprint("│") + createRow("Kubernetes Context", kubeContext)) diff --git a/internal/deployer/openshift.go b/internal/deployer/openshift.go new file mode 100644 index 0000000..22b6ae4 --- /dev/null +++ b/internal/deployer/openshift.go @@ -0,0 +1,125 @@ +package deployer + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + + "github.com/stackrox/roxie/internal/dockerauth" + v1 "k8s.io/api/core/v1" + k8sapierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/retry" +) + +const ( + openshiftConfigNamespace = "openshift-config" + openshiftGlobalPullSecretName = "pull-secret" + dockerConfigJsonKey = ".dockerconfigjson" + registryForDownstreamImages = "quay.io/rhacs-eng" +) + +var ( + namespacedGlobalPullSecret = openshiftConfigNamespace + "/" + openshiftGlobalPullSecretName +) + +// dockerConfigJSON represents the structure of a .dockerconfigjson secret value. +type dockerConfigJSON struct { + Auths map[string]dockerauth.AuthEntry `json:"auths"` +} + +// InjectGlobalOpenShiftPullSecret adds registry credentials to the OpenShift global pull secret. +func (d *Deployer) InjectGlobalOpenShiftPullSecret(ctx context.Context) error { + // Retry on Conflict, AlreadyExists, and NotFound to handle TOCTOU races between + // the Get and subsequent Create/Update (e.g., secret deleted after Get -> Update + // returns NotFound; secret created by another caller after Get -> Create returns + // AlreadyExists). + return retry.OnError(retry.DefaultRetry, func(err error) bool { + return k8sapierrors.IsConflict(err) || k8sapierrors.IsAlreadyExists(err) || k8sapierrors.IsNotFound(err) + }, func() error { + return d.injectGlobalOpenShiftPullSecretOnce(ctx) + }) +} + +func (d *Deployer) injectGlobalOpenShiftPullSecretOnce(ctx context.Context) error { + if d.dockerCreds == nil { + return errors.New("no pull secrets found") + } + credentials := *d.dockerCreds + + if d.k8sClient == nil { + return errors.New("k8s client not initialized") + } + + secrets := d.k8sClient.CoreV1().Secrets(openshiftConfigNamespace) + secret, err := secrets.Get(ctx, openshiftGlobalPullSecretName, metav1.GetOptions{}) + if err != nil { + if !k8sapierrors.IsNotFound(err) { + return fmt.Errorf("retrieving secret %s: %w", namespacedGlobalPullSecret, err) + } + secret = &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: openshiftGlobalPullSecretName, + Namespace: openshiftConfigNamespace, + }, + Type: v1.SecretTypeDockerConfigJson, + } + } + + modified, err := injectRegistryCredentialsIntoSecret(credentials, secret) + if err != nil { + return fmt.Errorf("injecting registry credentials into Kubernetes secret: %w", err) + } + if !modified { + d.logger.Dimf("Global pull secret %s already contains entry for %s, skipping", namespacedGlobalPullSecret, registryForDownstreamImages) + return nil + } + + if secret.ResourceVersion == "" { + if _, err := secrets.Create(ctx, secret, metav1.CreateOptions{}); err != nil { + return fmt.Errorf("creating secret %s: %w", namespacedGlobalPullSecret, err) + } + } else { + if _, err := secrets.Update(ctx, secret, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("updating secret %s: %w", namespacedGlobalPullSecret, err) + } + } + + d.logger.Successf("Injected pull secret for %s into %s", registryForDownstreamImages, namespacedGlobalPullSecret) + return nil +} + +// injectRegistryCredentialsIntoSecret mutates the secret in place, returning true if it was modified. +func injectRegistryCredentialsIntoSecret(credentials dockerauth.Credentials, secret *v1.Secret) (bool, error) { + var cfg dockerConfigJSON + if data, ok := secret.Data[dockerConfigJsonKey]; ok { + if err := json.Unmarshal(data, &cfg); err != nil { + return false, fmt.Errorf("unmarshaling %q in %s: %w", dockerConfigJsonKey, namespacedGlobalPullSecret, err) + } + } + if cfg.Auths == nil { + cfg.Auths = make(map[string]dockerauth.AuthEntry) + } + + if _, ok := cfg.Auths[registryForDownstreamImages]; ok { + return false, nil + } + + cfg.Auths[registryForDownstreamImages] = dockerauth.AuthEntry{ + Auth: base64.StdEncoding.EncodeToString([]byte(credentials.Username + ":" + credentials.Password)), + } + + updated, err := json.Marshal(cfg) + if err != nil { + return false, fmt.Errorf("marshaling updated docker config: %w", err) + } + + if secret.Data == nil { + secret.Data = make(map[string][]byte) + } + secret.Data[dockerConfigJsonKey] = updated + + return true, nil +} diff --git a/internal/deployer/openshift_test.go b/internal/deployer/openshift_test.go new file mode 100644 index 0000000..ae3d7b1 --- /dev/null +++ b/internal/deployer/openshift_test.go @@ -0,0 +1,118 @@ +package deployer + +import ( + "encoding/base64" + "encoding/json" + "testing" + + "github.com/stackrox/roxie/internal/dockerauth" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + v1 "k8s.io/api/core/v1" +) + +func TestInjectRegistryCredentialsIntoSecret(t *testing.T) { + const ( + registryUsername = "user" + registryPassword = "pass" + ) + + makeSecret := func(credentials map[string]map[string]string) *v1.Secret { + data, err := json.Marshal(map[string]any{ + "auths": credentials, + }) + require.NoError(t, err) + return &v1.Secret{Data: map[string][]byte{dockerConfigJsonKey: data}} + } + + encodeCredentials := func(username, password string) string { + return base64.StdEncoding.EncodeToString([]byte(username + ":" + password)) + } + + tests := []struct { + name string + secret *v1.Secret + expectModified bool + expectError bool + expectCredentials map[string]map[string]string + }{ + { + name: "injects into empty auths", + secret: makeSecret(nil), + expectModified: true, + expectCredentials: map[string]map[string]string{ + registryForDownstreamImages: { + "auth": encodeCredentials(registryUsername, registryPassword), + }, + }, + }, + { + name: "preserves existing entries", + secret: makeSecret(map[string]map[string]string{ + "registry.example.com": { + "auth": encodeCredentials("other", "secret"), + }, + }), + expectModified: true, + expectCredentials: map[string]map[string]string{ + "registry.example.com": { + "auth": encodeCredentials("other", "secret"), + }, + registryForDownstreamImages: { + "auth": encodeCredentials(registryUsername, registryPassword), + }, + }, + }, + { + name: "skips if already present", + secret: makeSecret(map[string]map[string]string{ + registryForDownstreamImages: { + "auth": encodeCredentials("existing", "existing"), + }, + }), + expectModified: false, + expectCredentials: map[string]map[string]string{ + registryForDownstreamImages: { + "auth": encodeCredentials("existing", "existing"), + }, + }, + }, + { + name: "handles nil secret data", + secret: &v1.Secret{}, + expectModified: true, + expectCredentials: map[string]map[string]string{ + registryForDownstreamImages: { + "auth": encodeCredentials(registryUsername, registryPassword), + }, + }, + }, + { + name: "returns error on invalid JSON", + secret: &v1.Secret{Data: map[string][]byte{dockerConfigJsonKey: []byte("not json")}}, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + creds := dockerauth.Credentials{Username: registryUsername, Password: registryPassword} + modified, err := injectRegistryCredentialsIntoSecret(creds, tt.secret) + if tt.expectError { + assert.Error(t, err) + return + } + require.NoError(t, err) + assert.Equal(t, tt.expectModified, modified) + + var cfg dockerConfigJSON + require.NoError(t, json.Unmarshal(tt.secret.Data[dockerConfigJsonKey], &cfg)) + + assert.Equal(t, len(tt.expectCredentials), len(cfg.Auths), "credential length mismatch") + + for regName, regCredentials := range tt.expectCredentials { + assert.Equal(t, regCredentials["auth"], cfg.Auths[regName].Auth, "credentials mismatch for registry %s", regName) + } + }) + } +} diff --git a/internal/deployer/operator.go b/internal/deployer/operator.go index 8f451c4..106476b 100644 --- a/internal/deployer/operator.go +++ b/internal/deployer/operator.go @@ -14,7 +14,6 @@ import ( "gopkg.in/yaml.v3" - "github.com/stackrox/roxie/internal/env" "github.com/stackrox/roxie/internal/k8s" "github.com/stackrox/roxie/internal/ocihelper" "github.com/stackrox/roxie/internal/types" @@ -202,7 +201,7 @@ func (d *Deployer) getOperatorBundleImage() string { // ensureKonfluxImageRewriting configures image rewriting for Konflux images func (d *Deployer) ensureKonfluxImageRewriting(ctx context.Context) error { - if !env.GetCurrentClusterType().IsOpenShift() { + if !d.config.Roxie.ClusterType.IsOpenShift() { return errors.New("image rewriting for Konflux is only supported on OpenShift4 clusters") } @@ -290,7 +289,7 @@ func (d *Deployer) applyImageContentSourcePolicy(ctx context.Context) error { // removeKonfluxImageRewriting removes the ImageContentSourcePolicy for Konflux images if it exists func (d *Deployer) removeKonfluxImageRewriting(ctx context.Context) error { - if !env.GetCurrentClusterType().IsOpenShift() { + if !d.config.Roxie.ClusterType.IsOpenShift() { return nil } @@ -320,7 +319,7 @@ func (d *Deployer) deployOperatorFromCSV(ctx context.Context, bundleDir string) } serviceAccountName := deploymentSpec["service_account"].(string) - d.useOperatorPullSecrets = d.config.Roxie.KonfluxImages && env.GetCurrentClusterType() != types.ClusterTypeInfraOpenShift4 + d.useOperatorPullSecrets = d.config.Roxie.KonfluxImages && d.config.Roxie.ClusterType != types.ClusterTypeInfraOpenShift4 d.logger.Info("📋 Operator deployment plan:") d.logger.Dim(fmt.Sprintf(" • Namespace: %s", operatorNamespace)) diff --git a/internal/deployer/operator_olm.go b/internal/deployer/operator_olm.go index 99fc63f..9428cac 100644 --- a/internal/deployer/operator_olm.go +++ b/internal/deployer/operator_olm.go @@ -13,11 +13,12 @@ import ( ) const ( - catalogSourceName = "stackrox-operator-index" - subscriptionName = "stackrox-operator-subscription" - operatorGroupName = "all-namespaces-operator-group" - operatorChannel = "latest" - operatorIndexImage = "quay.io/rhacs-eng/stackrox-operator-index" + catalogSourceName = "stackrox-operator-index" + subscriptionName = "stackrox-operator-subscription" + operatorGroupName = "all-namespaces-operator-group" + operatorChannel = "latest" + operatorIndexImage = "quay.io/rhacs-eng/stackrox-operator-index" + namespacedSubscriptionName = operatorNamespace + "/" + subscriptionName ) // OperatorDeploymentMode represents how the operator is deployed @@ -65,7 +66,7 @@ func (d *Deployer) deployOperatorViaOLM(ctx context.Context) error { } if err := d.waitForOperatorReady(ctx, operatorNamespace, operatorDeploymentName, 300); err != nil { - return fmt.Errorf("failed waiting for operator: %w", err) + return fmt.Errorf("failed waiting for operator in namespace %s to become ready: %w", operatorNamespace, err) } d.logger.Success("🎉 Operator deployed successfully via OLM!") @@ -226,7 +227,7 @@ func (d *Deployer) createSubscription(ctx context.Context) error { Stdin: bytes.NewReader(yamlData), }) if err != nil { - return fmt.Errorf("failed to create Subscription: %w", err) + return fmt.Errorf("failed to create Subscription %s: %w", namespacedSubscriptionName, err) } d.logger.Success("✓ Subscription created") @@ -253,9 +254,7 @@ func (d *Deployer) waitForAndApproveInstallPlan(ctx context.Context) error { } if time.Since(start) >= timeout { - // TODO(ROX-34499): some more info on what was wrong would be useful: a dump of the - // subscription or at least its name so that the user can investigate - return errors.New("timeout waiting for InstallPlan to be created") + return fmt.Errorf("timeout waiting for InstallPlan to be created for Subscription %s", namespacedSubscriptionName) } // Sanity check:Verify currentCSV matches expected version. @@ -264,12 +263,12 @@ func (d *Deployer) waitForAndApproveInstallPlan(ctx context.Context) error { Args: []string{"get", "subscription", subscriptionName, "-n", operatorNamespace, "-o", "jsonpath={.status.currentCSV}"}, }) if err != nil { - return fmt.Errorf("failed to get current CSV from subscription: %w", err) + return fmt.Errorf("failed to get current CSV from Subscription %s: %w", namespacedSubscriptionName, err) } currentCSV := strings.TrimSpace(result.Stdout) if currentCSV != expectedCSV { - return fmt.Errorf("subscription progressing to unexpected CSV '%s', expected '%s'", currentCSV, expectedCSV) + return fmt.Errorf("detected Subscription %s progressing to unexpected CSV '%s', expected '%s'", namespacedSubscriptionName, currentCSV, expectedCSV) } // Get InstallPlan name. @@ -277,7 +276,7 @@ func (d *Deployer) waitForAndApproveInstallPlan(ctx context.Context) error { Args: []string{"get", "subscription", subscriptionName, "-n", operatorNamespace, "-o", "jsonpath={.status.installPlanRef.name}"}, }) if err != nil { - return fmt.Errorf("failed to get InstallPlan name: %w", err) + return fmt.Errorf("failed to get InstallPlan name from Subscription %s: %w", namespacedSubscriptionName, err) } installPlanName := strings.TrimSpace(result.Stdout) @@ -292,7 +291,7 @@ func (d *Deployer) waitForAndApproveInstallPlan(ctx context.Context) error { Args: []string{"patch", "installplan", installPlanName, "-n", operatorNamespace, "--type", "merge", "-p", `{"spec":{"approved":true}}`}, }) if err != nil { - return fmt.Errorf("failed to approve InstallPlan: %w", err) + return fmt.Errorf("failed to approve InstallPlan %s for Subscription %s: %w", installPlanName, namespacedSubscriptionName, err) } d.logger.Success("✓ InstallPlan approved") @@ -325,8 +324,7 @@ func (d *Deployer) waitForCSVSuccess(ctx context.Context) error { time.Sleep(5 * time.Second) } - // TODO(ROX-34499): same as above - return fmt.Errorf("timeout waiting for CSV to succeed") + return fmt.Errorf("timeout waiting for CSV %s to succeed", csvName) } // detectOperatorDeploymentMode detects how the operator is currently deployed. diff --git a/internal/env/env_test.go b/internal/env/env_test.go index 8e0fed7..b9ccdb7 100644 --- a/internal/env/env_test.go +++ b/internal/env/env_test.go @@ -291,7 +291,7 @@ func TestClusterTypeString(t *testing.T) { { name: "types.ClusterTypeInfraGKE", clusterType: types.ClusterTypeInfraGKE, - want: "GKE", + want: "GKE (infra)", }, { name: "InfraOpenShift4", diff --git a/internal/k8s/client.go b/internal/k8s/client.go new file mode 100644 index 0000000..ba9a395 --- /dev/null +++ b/internal/k8s/client.go @@ -0,0 +1,27 @@ +package k8s + +import ( + "fmt" + + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/tools/clientcmd" +) + +func NewClient(kubeContext string) (kubernetes.Interface, error) { + clientConfigLoader := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + clientcmd.NewDefaultClientConfigLoadingRules(), + &clientcmd.ConfigOverrides{CurrentContext: kubeContext}, + ) + + restConfig, err := clientConfigLoader.ClientConfig() + if err != nil { + return nil, fmt.Errorf("failed to build kubernetes client config: %w", err) + } + + client, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return nil, fmt.Errorf("failed to create kubernetes client: %w", err) + } + + return client, nil +} diff --git a/internal/portforward/portforward.go b/internal/portforward/portforward.go index b62f69c..410dede 100644 --- a/internal/portforward/portforward.go +++ b/internal/portforward/portforward.go @@ -100,13 +100,8 @@ func (m *Manager) Start(namespace, serviceName string, remotePort, preferredLoca // Wait for port to be ready if !m.waitTCPReady("127.0.0.1", localPort, 20.0) { - // Kill the process group if cmd.Process != nil { - pgid, err := syscall.Getpgid(cmd.Process.Pid) - if err == nil { - syscall.Kill(-pgid, syscall.SIGTERM) - } - // TODO(ROX-34499): AFAICT this can get stuck forever if the process blocks SIGTERM... + cmd.Process.Kill() cmd.Wait() } return "", fmt.Errorf("port-forward did not become ready") diff --git a/internal/types/cluster_type.go b/internal/types/cluster_type.go index 47146a2..1386673 100644 --- a/internal/types/cluster_type.go +++ b/internal/types/cluster_type.go @@ -1,25 +1,27 @@ package types +import "fmt" + // ClusterType represents different types of Kubernetes clusters -type ClusterType int +type ClusterType string const ( // ClusterTypeUnknown represents an unidentified cluster type - ClusterTypeUnknown ClusterType = iota + ClusterTypeUnknown ClusterType = "Unknown" // ClusterTypeInfraGKE represents a GKE cluster created via Infra. - ClusterTypeInfraGKE + ClusterTypeInfraGKE ClusterType = "InfraGKE" // ClusterTypeInfraOpenShift4 represents an OpenShift 4 cluster - ClusterTypeInfraOpenShift4 + ClusterTypeInfraOpenShift4 ClusterType = "InfraOpenShift4" // Generic OpenShift4 cluster (e.g. for prow CI) - ClusterTypeOpenShift4 + ClusterTypeOpenShift4 ClusterType = "OpenShift4" // ClusterTypeKind represents a Kind (Kubernetes in Docker) cluster - ClusterTypeKind + ClusterTypeKind ClusterType = "Kind" // ClusterTypeMinikube represents a Minikube cluster - ClusterTypeMinikube + ClusterTypeMinikube ClusterType = "Minikube" // ClusterTypeK3s represents a K3s cluster - ClusterTypeK3s + ClusterTypeK3s ClusterType = "K3s" // ClusterTypeCRC represents a CRC (CodeReady Containers) cluster - ClusterTypeCRC + ClusterTypeCRC ClusterType = "CRC" ) func (ct ClusterType) IsOpenShift() bool { @@ -30,7 +32,7 @@ func (ct ClusterType) IsOpenShift() bool { func (ct ClusterType) String() string { switch ct { case ClusterTypeInfraGKE: - return "GKE" + return "GKE (infra)" case ClusterTypeInfraOpenShift4: return "OpenShift4 (infra)" case ClusterTypeOpenShift4: @@ -38,11 +40,11 @@ func (ct ClusterType) String() string { case ClusterTypeKind: return "Kind" case ClusterTypeMinikube: - return "minikube" + return "Minikube" case ClusterTypeK3s: - return "k3s" + return "K3s" case ClusterTypeCRC: - return "crc" + return "CRC" default: return "Unknown" } @@ -59,3 +61,20 @@ func AllClusterTypes() []ClusterType { ClusterTypeOpenShift4, } } + +func (ct *ClusterType) UnmarshalYAML(unmarshal func(any) error) error { + var s string + if err := unmarshal(&s); err != nil { + return err + } + + var sAsClusterType = ClusterType(s) + + for _, valid := range AllClusterTypes() { + if sAsClusterType == valid { + *ct = valid + return nil + } + } + return fmt.Errorf("unknown cluster type identifier: %q", s) +} diff --git a/internal/types/cluster_type_test.go b/internal/types/cluster_type_test.go new file mode 100644 index 0000000..127d78a --- /dev/null +++ b/internal/types/cluster_type_test.go @@ -0,0 +1,73 @@ +package types + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +func TestClusterTypeMarshalYAML(t *testing.T) { + tests := []struct { + clusterType ClusterType + expected string + }{ + {ClusterTypeInfraGKE, "InfraGKE"}, + {ClusterTypeInfraOpenShift4, "InfraOpenShift4"}, + {ClusterTypeOpenShift4, "OpenShift4"}, + {ClusterTypeKind, "Kind"}, + {ClusterTypeMinikube, "Minikube"}, + {ClusterTypeK3s, "K3s"}, + {ClusterTypeCRC, "CRC"}, + } + for _, tt := range tests { + t.Run(tt.expected, func(t *testing.T) { + out, err := yaml.Marshal(tt.clusterType) + require.NoError(t, err) + assert.Equal(t, tt.expected+"\n", string(out)) + }) + } +} + +func TestClusterTypeUnmarshalYAML(t *testing.T) { + tests := []struct { + input string + expected ClusterType + }{ + {"InfraGKE", ClusterTypeInfraGKE}, + {"InfraOpenShift4", ClusterTypeInfraOpenShift4}, + {"OpenShift4", ClusterTypeOpenShift4}, + {"Kind", ClusterTypeKind}, + {"Minikube", ClusterTypeMinikube}, + {"K3s", ClusterTypeK3s}, + {"CRC", ClusterTypeCRC}, + } + for _, tt := range tests { + t.Run(tt.input, func(t *testing.T) { + var ct ClusterType + err := yaml.Unmarshal([]byte(tt.input), &ct) + require.NoError(t, err) + assert.Equal(t, tt.expected, ct) + }) + } +} + +func TestClusterTypeUnmarshalYAML_Invalid(t *testing.T) { + var ct ClusterType + err := yaml.Unmarshal([]byte("bogus"), &ct) + assert.ErrorContains(t, err, "unknown cluster type identifier") +} + +func TestClusterTypeRoundTrip(t *testing.T) { + for _, ct := range AllClusterTypes() { + t.Run(ct.String(), func(t *testing.T) { + out, err := yaml.Marshal(ct) + require.NoError(t, err) + + var parsed ClusterType + require.NoError(t, yaml.Unmarshal(out, &parsed)) + assert.Equal(t, ct, parsed) + }) + } +}