diff --git a/ci/Containerfile b/ci/Containerfile index cf47cf7..658ecfb 100644 --- a/ci/Containerfile +++ b/ci/Containerfile @@ -1,24 +1,6 @@ # CI build-root image for rosa-regional-platform-cli PR checks. # Used by ci-operator via build_root.project_image.dockerfile_path. +FROM registry.ci.openshift.org/openshift/release:rhel-9-release-golang-1.26-openshift-4.23 -FROM registry.ci.openshift.org/openshift/release:rhel-9-release-golang-1.25-openshift-4.22 - -ARG GOLANGCI_LINT_VERSION=v1.62.2 - -# Install golangci-lint using go install to ensure it's built with Go 1.25 -# This avoids "Go language version used to build golangci-lint is lower than targeted version" errors -# golangci-lint install must not use vendor-only mode from the base image; allow -# resolving modules for this one command only (not as a global default for CI). -RUN GOFLAGS=-mod=mod go install github.com/golangci/golangci-lint/cmd/golangci-lint@${GOLANGCI_LINT_VERSION} && \ -golangci-lint --version - -# Pre-create cache directories writable by any UID (OpenShift runs as random UID) -RUN mkdir -p /tmp/.golangci-lint-cache /tmp/.gocache /tmp/.gomodcache && \ -chmod -R 777 /tmp/.golangci-lint-cache /tmp/.gocache /tmp/.gomodcache - -# Default for repo checks: refuse silent go.mod/go.sum updates. Use GOFLAGS=-mod=mod -# only on commands that intentionally modify modules (e.g. go mod tidy). -ENV GOFLAGS="-mod=readonly" -ENV GOLANGCI_LINT_CACHE=/tmp/.golangci-lint-cache -ENV GOCACHE=/tmp/.gocache -ENV GOMODCACHE=/tmp/.gomodcache +# Allow Go toolchain auto-download for go.mod version requirements (1.26.3) +ENV GOTOOLCHAIN=auto diff --git a/docs/demo/demo.md b/docs/demo/demo.md new file mode 100644 index 0000000..1173d93 --- /dev/null +++ b/docs/demo/demo.md @@ -0,0 +1,38 @@ +rosactl cluster kubeconfig 8372824b-1131-4c26-91fc-9736694dcd19 +```bash +apiVersion: v1 +kind: Config +clusters: + - cluster: + server: https://api.cdoan-hcp100.8372.0.us-east-1-eph-4b8b842d.dev0.rosa.devshift.net:443 + name: cdoan-hcp100 +users: + - name: cdoan-hcp100-iam + user: + exec: + apiVersion: client.authentication.k8s.io/v1 + interactiveMode: Never + command: /Users/cdoan/.local/bin/rosactl + args: + - cluster + - get-token + - --cluster-id + - 8372824b-1131-4c26-91fc-9736694dcd19 + - --region + - us-east-1 +contexts: + - context: + cluster: cdoan-hcp100 + user: cdoan-hcp100-iam + name: cdoan-hcp100 +current-context: cdoan-hcp100 +``` + +# set the kubeconfig +export KUBECONFIG=/tmp/cdoan-hcp100.kube + +# switch to the aws account +export AWS_PROFILE=rrp-chris-regional_cluster + +# verify we can create pods +kubectl create deployment demo-nginx --image=nginx --replicas=3 \ No newline at end of file diff --git a/go.mod b/go.mod index c1e4f6d..93b9c8d 100644 --- a/go.mod +++ b/go.mod @@ -1,56 +1,92 @@ module github.com/openshift-online/rosa-regional-platform-cli -go 1.25.8 +go 1.26.3 require ( github.com/aws/aws-lambda-go v1.54.0 - github.com/aws/aws-sdk-go-v2 v1.43.0 + github.com/aws/aws-sdk-go-v2 v1.43.5 github.com/aws/aws-sdk-go-v2/config v1.32.22 - github.com/aws/aws-sdk-go-v2/credentials v1.19.21 + github.com/aws/aws-sdk-go-v2/credentials v1.19.30 github.com/aws/aws-sdk-go-v2/service/cloudformation v1.71.15 github.com/aws/aws-sdk-go-v2/service/ec2 v1.308.0 github.com/aws/aws-sdk-go-v2/service/ecr v1.58.2 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.36.0 + github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.58.1 github.com/aws/aws-sdk-go-v2/service/iam v1.54.2 github.com/aws/aws-sdk-go-v2/service/lambda v1.92.1 github.com/aws/aws-sdk-go-v2/service/route53 v1.65.2 - github.com/aws/aws-sdk-go-v2/service/sts v1.43.1 - github.com/aws/smithy-go v1.27.3 + github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 + github.com/aws/smithy-go v1.27.7 github.com/onsi/ginkgo/v2 v2.29.0 github.com/onsi/gomega v1.42.1 + github.com/openshift-online/rosa-hyperfleet-api/api v0.0.0 + github.com/openshift-online/rosa-hyperfleet-api/clientset v0.0.0-00010101000000-000000000000 + github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16 github.com/spf13/cobra v1.10.2 + k8s.io/apimachinery v0.36.0 ) require ( github.com/Masterminds/semver/v3 v3.4.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 // indirect - github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 // indirect - github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28 // indirect - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.36.0 // indirect - github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.58.1 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 // indirect - github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 // indirect - github.com/aws/aws-sdk-go-v2/service/signin v1.1.3 // indirect - github.com/aws/aws-sdk-go-v2/service/sso v1.31.1 // indirect - github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 // indirect + github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/go-logr/logr v1.4.3 // indirect + github.com/emicklei/go-restful/v3 v3.13.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.2 // indirect + github.com/go-logr/logr v1.4.4 // 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/go-task/slim-sprig/v3 v3.0.0 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/rogpeppe/go-internal v1.14.1 // 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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // 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/openshift/api v0.0.0-20260416105050-3c6b218b8a80 // indirect github.com/spf13/pflag v1.0.10 // indirect - github.com/stretchr/testify v1.11.1 // indirect + github.com/x448/float16 v0.8.4 // indirect + go.yaml.in/yaml/v2 v2.4.4 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.36.0 // indirect golang.org/x/net v0.56.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect golang.org/x/sync v0.21.0 // indirect golang.org/x/sys v0.46.0 // indirect + golang.org/x/term v0.44.0 // indirect golang.org/x/text v0.38.0 // indirect + golang.org/x/time v0.14.0 // indirect golang.org/x/tools v0.45.0 // indirect google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // indirect - gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect + k8s.io/api v0.36.0 // indirect + k8s.io/client-go v0.36.0 // indirect + k8s.io/klog/v2 v2.140.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect + k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3 // 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.2 // indirect + sigs.k8s.io/yaml v1.6.0 // indirect +) + +replace ( + github.com/openshift-online/rosa-hyperfleet-api/api => github.com/openshift-online/rosa-hyperfleet-api/api v0.0.0-20260813062239-c882180ff9a1 + github.com/openshift-online/rosa-hyperfleet-api/clientset => github.com/openshift-online/rosa-hyperfleet-api/clientset v0.0.0-20260813062239-c882180ff9a1 ) diff --git a/go.sum b/go.sum index 8738198..a074767 100644 --- a/go.sum +++ b/go.sum @@ -2,22 +2,22 @@ github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1 github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/aws/aws-lambda-go v1.54.0 h1:EGYpdyRGF88xszqlGcBewz811mJeRS+maNlLZXFheII= github.com/aws/aws-lambda-go v1.54.0/go.mod h1:dpMpZgvWx5vuQJfBt0zqBha60q7Dd7RfgJv23DymV8A= -github.com/aws/aws-sdk-go-v2 v1.43.0 h1:fharf/WhbRAVZ1du0QL7roNFxZ6T/sWr+4Ni617bwSI= -github.com/aws/aws-sdk-go-v2 v1.43.0/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM= +github.com/aws/aws-sdk-go-v2 v1.43.5 h1:yKT5GYnFWhuDo+DqKvE5ZPwVn3RjC4MAeBtZGlh6AVM= +github.com/aws/aws-sdk-go-v2 v1.43.5/go.mod h1:wZjAJppCntyOGgVSmgVTfDyRJK5PHOasO6Wsy8U7Axk= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12 h1:oRtsqWgxbpeXrOlxOoQStx2M9WNbIkPq4C4Xn1or6bc= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.12/go.mod h1:Zg0Oe9qT+9wcezlm1a64wGJp2qZdRElVxo/seJf7jYU= github.com/aws/aws-sdk-go-v2/config v1.32.22 h1:Vfvp7+fYKsVCADcWOEllqEV47aIBXhNchvyDFu1B5fY= github.com/aws/aws-sdk-go-v2/config v1.32.22/go.mod h1:0+H+0nPKbvWltf5vSIGkApv+hGbaQ4FfwTjGIYQREcw= -github.com/aws/aws-sdk-go-v2/credentials v1.19.21 h1:0+HscFXtNa4+3buV4IlG6v5lnOdzi5TrpicFGjKHgh4= -github.com/aws/aws-sdk-go-v2/credentials v1.19.21/go.mod h1:UE8+9t5zudFwu5k5ShC1PKArVEdOkQQdCXIHQAVNUcU= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27 h1:BEfN1sjtiKEdikRDxYkjZNE4tyvw/YbGWCbl3xDZgRw= -github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.27/go.mod h1:ISGSFNbOHRS+JV/17yStzRTPBUHHqF92kCpRLLyH3Nk= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31 h1:Z8F3hfCY33IGpJjFAnv0wvtv1FIKj1GHmRDEYqy64tw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.31/go.mod h1:aVyUoytEyOViR6jhq6jula0xkc5NfBE2hgeF6BvOrao= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31 h1:hyOxUyXdh3AyjE93gBgsfziJag9ACwcs+ZpDBLzi8mw= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.31/go.mod h1:OERqI9k0draSLB8O8woxY3q25ZWTELRK4RRoLMuMZFo= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28 h1:eaS9vwQ5ym4Y9S6+G/K3d3lgZhxs9Sldcn/YS7cmdKY= -github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.28/go.mod h1:oTdbDr+BMs7gAYrNpD0LDTyqQfv6yOYgTDv46+xbwFY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.30 h1:TTCvvzFU6gXa4iJecNG/0F/B0oYTiazoRECr2XyLHrY= +github.com/aws/aws-sdk-go-v2/credentials v1.19.30/go.mod h1:jKxAp2AEncnliinzpgOSZDFv6+VjvWhjw/AtbfsWT9U= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31 h1:kfVL5wAunCJycL6MOQ6aNh6PlAYEymflcjuKmrWUA0o= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.31/go.mod h1:nWfRNDAppujCQgOUd43lKT4yeLv9z3nJ3bw1G3BgQKo= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36 h1:5CrzwxDqf4w3x1Vs3/NiZ0nsC34Hbm3pIDMWbsLebOE= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.36/go.mod h1:A3gHdKZIvG/QXERzZwcxNS3RNDFcRCuhhTFBYp+V/nw= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36 h1:A4N2f4YPcST0v+dWtX+xrpPPCL9VTBhoIFFUWYqbacE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.36/go.mod h1:B/Qr859uxWUEfZeGotK5KAEoof4Q9YWgNtPSwV6jcyk= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32 h1:0MrUL35H/Y4kdFfItoR5jCgtDQ4Z/8LudAoIHRfA4hE= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.32/go.mod h1:2tNZkuWz54arj8mHVf+8Y7cKkcD8Wr/fBpENgEXpjLc= github.com/aws/aws-sdk-go-v2/service/cloudformation v1.71.15 h1:us9kmxnViaD2sbiIKXzSOGZ4c14C5aLGV+EbnccThF4= github.com/aws/aws-sdk-go-v2/service/cloudformation v1.71.15/go.mod h1:H2CEB5hS3tOB3f6/1x/6iuMpktmS6Wei9ylWTBMpGe8= github.com/aws/aws-sdk-go-v2/service/ec2 v1.308.0 h1:xBP+yWpveXD/PxK7HRMcoG6yj1vdOjSahAg4qPomF+0= @@ -30,47 +30,71 @@ github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.58.1 h1:4KFrMyyOV github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.58.1/go.mod h1:8oLCSyyky75OpQ8Lo72GuoJXbeaEb1R8+dHgQOgWKaI= github.com/aws/aws-sdk-go-v2/service/iam v1.54.2 h1:gJxdCZtnmAX9jF1RjBVJ6O2Bn4P2zLAQFXYmTBM+W5A= github.com/aws/aws-sdk-go-v2/service/iam v1.54.2/go.mod h1:GO5Zea33HKIxG12xqmrzTFgEZcok4jdpc4aM2TcHTfk= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12 h1:ZD2+BSw9vFsNlKYIasSNt3uDbjqqXIBcM13UJv/Lx2k= -github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.12/go.mod h1:Ms4zlcVBbXbiP7EVLhl+lgjvA/a7YphqQ3Ih3174EmI= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29 h1:DRebniUGZ2MqiiIVmQJ04vIXr918hubdHMnarSLEWyU= -github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.29/go.mod h1:LfRkPCD8YHDM2E5eTkos2UpwYeZnBcVarTa8L59bJHA= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13 h1:mbRIur/BiHK6SKPjoBIXSE/hJ6g6JGRLuxQy1jGjlN4= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.13/go.mod h1:ITg9em2KbJx1s0y4aqRX5OYWG6HBZ5TVR//OdpEZ2CQ= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31 h1:w2SIhW92DZPFrSL4ksVCr8IYff5OZwIcxg8+95tzvAI= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.31/go.mod h1:wAhpCQbkov+IcvjozJbd2xRCoZybUEHNkcFunssNACg= github.com/aws/aws-sdk-go-v2/service/lambda v1.92.1 h1:WXCHC8eGCieCTxSJ0CsZc4yRK8PiUKPZm3un8L4Eu6I= github.com/aws/aws-sdk-go-v2/service/lambda v1.92.1/go.mod h1:RQmeW7HZrBCTGhwnmRTEw29G+Fhao0pkhrlwakALzbs= github.com/aws/aws-sdk-go-v2/service/route53 v1.65.2 h1:/6WibgFHIQnBuP0PtWnz7NZ6DZ0/mN9ua5kruz7UXMA= github.com/aws/aws-sdk-go-v2/service/route53 v1.65.2/go.mod h1:kg30QdUv8hG6jifkHp+F8448US9y9a+6xS2l5F8aa38= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.3 h1:t6U7sowMfOjTeZXtDOtgEJXsoJyX4MDag+sfWGwUM9M= -github.com/aws/aws-sdk-go-v2/service/signin v1.1.3/go.mod h1:WhO1EH3phjFWValQDsExaxncgEWJsHeoTvuyQAj3jwU= -github.com/aws/aws-sdk-go-v2/service/sso v1.31.1 h1:TUV8oytPCX1PfVgZn0N8/sPZx7T0YasaMCBHox1erlw= -github.com/aws/aws-sdk-go-v2/service/sso v1.31.1/go.mod h1:tEL1hqCrkgwrDVL04HuLxz1SLUXdh+4kKhWv1pXKeiY= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4 h1:p9+Fizo2sUB6wI5Yb3K5lmykQAGs5JrKLBV/me6613Y= -github.com/aws/aws-sdk-go-v2/service/ssooidc v1.36.4/go.mod h1:0x10Wy0dVS4Gn552xhHY5th2QdYpfJf44EsfyYGV194= -github.com/aws/aws-sdk-go-v2/service/sts v1.43.1 h1:r/vUkpLilfCA3sxbRnkHbJejaoVHEdj4FEhv+Zva4DU= -github.com/aws/aws-sdk-go-v2/service/sts v1.43.1/go.mod h1:t01JURC8Fe5M+7R1K0vzIZ2NT04HqvZR+FjlHrHDT2A= -github.com/aws/smithy-go v1.27.3 h1:F3Zb497UhhskkfpJmfkXswyo+t0sh9OTBnIHjogWbVY= -github.com/aws/smithy-go v1.27.3/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.0 h1:OHH5iTQvVGmfHjX/5Q+vFuA/Rf2x6/95aJ/75QCQSm4= +github.com/aws/aws-sdk-go-v2/service/signin v1.5.0/go.mod h1:mCF3AK9PpL49oOrhniUXWAfhVBVQ/XbytoE5eccZUIs= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.0 h1:CaJyYhxBE0M/HJX/YvSaSmQlsI91VHB0lKU8LtLxL3A= +github.com/aws/aws-sdk-go-v2/service/sso v1.33.0/go.mod h1:+e6BMRMPjBQoCw/WovYR9GLy2IU0z4Q77smOB1DraSg= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0 h1:tC323YV77QdafeBr6LUhLDTsboyuyHLNRwAyCP44kGU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.38.0/go.mod h1:SfLK1sgviHmbI+MozR9iDwDjL4cdCVZtahsjoR+z7wg= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.0 h1:Pd6PNlp4t8PTXxqzstICl52Wsy78vpjFZ7PRUj44mJc= +github.com/aws/aws-sdk-go-v2/service/sts v1.45.0/go.mod h1:rmQ0TnHzuLPmabgjPcsywhsSOmaBDgzR4zvDxSPsGdg= +github.com/aws/smithy-go v1.27.7 h1:Zgj5z4LfcDYoQIVk+n/yGdTkP/2y6ZT5vYxe0fp7bqE= +github.com/aws/smithy-go v1.27.7/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= 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/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.13.0 h1:C4Bl2xDndpU6nJ4bc1jXd+uTmYPVUwkD6bFY/oTyCes= +github.com/emicklei/go-restful/v3 v3.13.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/fxamacker/cbor/v2 v2.9.2 h1:X4Ksno9+x3cz0TZv69ec1hxP/+tymuR8PXQJyDwfh78= +github.com/fxamacker/cbor/v2 v2.9.2/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gkampitakis/ciinfo v0.3.2 h1:JcuOPk8ZU7nZQjdUhctuhQofk7BGHuIy0c9Ez8BNhXs= github.com/gkampitakis/ciinfo v0.3.2/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= github.com/gkampitakis/go-snaps v0.5.15 h1:amyJrvM1D33cPHwVrjo9jQxX8g/7E2wYdZ+01KS3zGE= github.com/gkampitakis/go-snaps v0.5.15/go.mod h1:HNpx/9GoKisdhw9AFOBT1N7DBs9DiHo/hGheFGBZ+mc= -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/logr v1.4.4 h1:tG4xh9yMsRCAiodLVTxyrkzSZ9+o0L1Kg/+cPVcbP/8= +github.com/go-logr/logr v1.4.4/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/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +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= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +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/joshdk/go-junit v1.0.0 h1:S86cUKIdwBHWwA6xCmFlf3RTLfVXYQfvanM5Uh+K6GE= github.com/joshdk/go-junit v1.0.0/go.mod h1:TiiV0PqkaNfFXjEiyjWM3XXrhVyCa1K4Zfga6W52ung= +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/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= @@ -78,14 +102,33 @@ 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/maruel/natural v1.1.1 h1:Hja7XhhmvEFhcByqDoHz9QZbkWey+COd9xWfCfn1ioo= github.com/maruel/natural v1.1.1/go.mod h1:v+Rfd79xlw1AgVBjbO0BEQmptqb5HvL/k9GRHB7ZKEg= github.com/mfridman/tparse v0.18.0 h1:wh6dzOKaIwkUGyKgOntDW4liXSo37qg5AXbIhkMV3vE= github.com/mfridman/tparse v0.18.0/go.mod h1:gEvqZTuCgEhPbYk/2lS3Kcxg1GmTxxU7kTC8DvP0i/A= +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/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/onsi/ginkgo/v2 v2.29.0 h1:rfh+ZFjgJhYWRoIqVf3Uwx/W20yLrcrE2h2GmYVRaag= github.com/onsi/ginkgo/v2 v2.29.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= github.com/onsi/gomega v1.42.1 h1:iN1rCUX+44NZ1Dc97MPoeFYbFR0vh8zxoxMFwKdyZ6I= github.com/onsi/gomega v1.42.1/go.mod h1:REff/hsDsodHoKlWsP2mAPhu1+5/6hVYNf9rIEBpeSg= +github.com/openshift-online/rosa-hyperfleet-api/api v0.0.0-20260813062239-c882180ff9a1 h1:XMvcHBILqWuYOZFfX1wzvEkSXAT36/ig0OLslxMWOz4= +github.com/openshift-online/rosa-hyperfleet-api/api v0.0.0-20260813062239-c882180ff9a1/go.mod h1:wLNKyreMacFyDUr+3BwjB3eNifpnMIXI6qxu9sQigA0= +github.com/openshift-online/rosa-hyperfleet-api/clientset v0.0.0-20260813062239-c882180ff9a1 h1:mlyqDMbJVFnRKUJLquGtnw+x+0P+ag16p7WXxGelGRo= +github.com/openshift-online/rosa-hyperfleet-api/clientset v0.0.0-20260813062239-c882180ff9a1/go.mod h1:igLlfXw8hx5KSLdt48v70wVaeLdoy8yNWoRi//p+WL8= +github.com/openshift/api v0.0.0-20260416105050-3c6b218b8a80 h1:r0S/yoZAI0iWo1JvoIijaIgWGWf/izg4WiV7Wrtz16k= +github.com/openshift/api v0.0.0-20260416105050-3c6b218b8a80/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo= +github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16 h1:QDSh3vkKYq7Fn9utYGlAJadkTdyaRl9IY7Cr6cNDAow= +github.com/openshift/hypershift/api v0.0.0-20260625052409-9acec4759a16/go.mod h1:Z3lkj5pFqY+KTl3Do9gXdEZdKWLnkUTSDShLD1HE0CM= +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= @@ -96,6 +139,15 @@ github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiT github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 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/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= @@ -106,18 +158,28 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= +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.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= +go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ= 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/mod v0.36.0 h1:JJjpVx6myfUsUdAzZuOSTTmRE0PfZeNWzzvKrP7amb4= golang.org/x/mod v0.36.0/go.mod h1:moc6ELqsWcOw5Ef3xVprK5ul/MvtVvkIXLziUOICjUQ= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.44.0 h1:0rLvDRCtNj0gZkyIXhCyOb2OAzEhLVqc4B+hrsBhrmc= +golang.org/x/term v0.44.0/go.mod h1:7ze4MdzUzLXpSAoFP1H0bOI9aXDqveSvatT5vKcFh2Y= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= +golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.45.0 h1:18qN3FAooORvApf5XjCXgsuayZOEtXf6JK18I3+ONa8= golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0= google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= @@ -125,5 +187,30 @@ google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+ 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= +k8s.io/api v0.36.0 h1:SgqDhZzHdOtMk40xVSvCXkP9ME0H05hPM3p9AB1kL80= +k8s.io/api v0.36.0/go.mod h1:m1LVrGPNYax5NBHdO+QuAedXyuzTt4RryI/qnmNvs34= +k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ= +k8s.io/apimachinery v0.36.0/go.mod h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc= +k8s.io/client-go v0.36.0 h1:pOYi7C4RHChYjMiHpZSpSbIM6ZxVbRXBy7CuiIwqA3c= +k8s.io/client-go v0.36.0/go.mod h1:ZKKcpwF0aLYfkHFCjillCKaTK/yBkEDHTDXCFY6AS9Y= +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-20260707023825-cf1189d6abe3 h1:jVkFFVfXdXP74B/zbO3hM3hpSFD0xvhQ5U686DPurkE= +k8s.io/utils v0.0.0-20260707023825-cf1189d6abe3/go.mod h1:M2s5JB1lIYP3jzZdorPLHXIPJzt9vv2muW5a6L9DtNM= +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.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/commands/cluster/api.go b/internal/commands/cluster/api.go deleted file mode 100644 index 713f21e..0000000 --- a/internal/commands/cluster/api.go +++ /dev/null @@ -1,132 +0,0 @@ -package cluster - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "encoding/json" - "fmt" - "io" - "net/http" - "time" - - awssdk "github.com/aws/aws-sdk-go-v2/aws" - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" -) - -var httpClient = &http.Client{Timeout: 15 * time.Second} - -func signedGet(ctx context.Context, url string, creds awssdk.Credentials, region string) ([]byte, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - payloadHash := sha256.Sum256([]byte("")) - payloadHashStr := hex.EncodeToString(payloadHash[:]) - - signer := v4.NewSigner() - if err := signer.SignHTTP(ctx, creds, req, payloadHashStr, "execute-api", region, time.Now()); err != nil { - return nil, fmt.Errorf("failed to sign request: %w", err) - } - - resp, err := httpClient.Do(req) - if err != nil { - return nil, fmt.Errorf("failed to execute request: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("failed to read response: %w", err) - } - - if resp.StatusCode != http.StatusOK { - return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) - } - - return body, nil -} - -func signedDelete(ctx context.Context, url string, creds awssdk.Credentials, region string) ([]byte, int, error) { - req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil) - if err != nil { - return nil, 0, fmt.Errorf("failed to create request: %w", err) - } - - payloadHash := sha256.Sum256([]byte("")) - payloadHashStr := hex.EncodeToString(payloadHash[:]) - - signer := v4.NewSigner() - if err := signer.SignHTTP(ctx, creds, req, payloadHashStr, "execute-api", region, time.Now()); err != nil { - return nil, 0, fmt.Errorf("failed to sign request: %w", err) - } - - resp, err := httpClient.Do(req) - if err != nil { - return nil, 0, fmt.Errorf("failed to execute request: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, resp.StatusCode, fmt.Errorf("failed to read response: %w", err) - } - - return body, resp.StatusCode, nil -} - -func fetchAPIURL(ctx context.Context, baseURL, clusterID string, creds awssdk.Credentials, region string) (string, error) { - endpoint := fmt.Sprintf("%s/api/v0/clusters/%s/statuses", baseURL, clusterID) - body, err := signedGet(ctx, endpoint, creds, region) - if err != nil { - return "", fmt.Errorf("failed to fetch cluster statuses: %w", err) - } - - var envelope struct { - Status *struct { - ControlPlaneEndpoint *struct { - Host string `json:"host"` - Port int32 `json:"port"` - } `json:"controlPlaneEndpoint"` - } `json:"status"` - } - if err := json.Unmarshal(body, &envelope); err != nil { - return "", fmt.Errorf("failed to parse cluster statuses: %w", err) - } - - if envelope.Status != nil && envelope.Status.ControlPlaneEndpoint != nil { - ep := envelope.Status.ControlPlaneEndpoint - if ep.Host != "" { - return fmt.Sprintf("https://%s:%d", ep.Host, ep.Port), nil - } - } - return "", nil -} - -func fetchClusterByName(ctx context.Context, baseURL, name string, creds awssdk.Credentials, region string) (*clusterItem, error) { - const pageSize = 100 - for offset := 0; ; offset += pageSize { - endpoint := fmt.Sprintf("%s/api/v0/clusters?limit=%d&offset=%d", baseURL, pageSize, offset) - body, err := signedGet(ctx, endpoint, creds, region) - if err != nil { - return nil, fmt.Errorf("failed to list clusters: %w", err) - } - - var resp listResponse - if err := json.Unmarshal(body, &resp); err != nil { - return nil, fmt.Errorf("failed to parse cluster list: %w", err) - } - - for _, c := range resp.Items { - if c.Name == name || c.ID == name { - return &c, nil - } - } - - if len(resp.Items) < pageSize { - break - } - } - return nil, fmt.Errorf("cluster %q not found", name) -} diff --git a/internal/commands/cluster/delete.go b/internal/commands/cluster/delete.go index 1b0ed32..cdf1b85 100644 --- a/internal/commands/cluster/delete.go +++ b/internal/commands/cluster/delete.go @@ -4,15 +4,16 @@ import ( "bufio" "context" "fmt" - "net/http" - "net/url" "os" "strings" "time" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public" + "github.com/openshift-online/rosa-hyperfleet-api/clientset/platform" "github.com/openshift-online/rosa-regional-platform-cli/internal/aws" - "github.com/openshift-online/rosa-regional-platform-cli/internal/config" + platformclient "github.com/openshift-online/rosa-regional-platform-cli/internal/platform" "github.com/spf13/cobra" + k8serrors "k8s.io/apimachinery/pkg/api/errors" ) type deleteOptions struct { @@ -49,34 +50,32 @@ Examples: } func runDeleteCluster(ctx context.Context, nameOrID string, opts *deleteOptions) error { - baseURL, err := config.GetPlatformAPIURL() - if err != nil { - return err - } - cfg, err := aws.NewConfig(ctx) if err != nil { return fmt.Errorf("failed to load AWS config: %w", err) } - creds, err := cfg.Credentials.Retrieve(ctx) - if err != nil { - return fmt.Errorf("failed to retrieve AWS credentials: %w", err) + if cfg.Region == "" { + return aws.ErrRegionRequired } - region := cfg.Region - if region == "" { - return aws.ErrRegionRequired + // Create the clientset + cs, err := platformclient.NewClientset(ctx, cfg) + if err != nil { + return fmt.Errorf("failed to create clientset: %w", err) } - // Resolve name → ID if needed (fetchClusterByName matches on both name and ID) - cluster, err := fetchClusterByName(ctx, baseURL, nameOrID, creds, region) + // Fetch cluster to resolve name → ID and get display name + cluster, err := getClusterByNameOrID(ctx, cs, nameOrID) if err != nil { return err } + clusterName := cluster.Name + clusterID := string(cluster.UID) + if !opts.yes { - fmt.Fprintf(os.Stderr, "Are you sure you want to delete cluster %q (ID: %s)? [y/N] ", cluster.Name, cluster.ID) + fmt.Fprintf(os.Stderr, "Are you sure you want to delete cluster %q (ID: %s)? [y/N] ", clusterName, clusterID) reader := bufio.NewReader(os.Stdin) answer, err := reader.ReadString('\n') if err != nil { @@ -88,48 +87,40 @@ func runDeleteCluster(ctx context.Context, nameOrID string, opts *deleteOptions) } } - endpoint := fmt.Sprintf("%s/api/v0/clusters/%s", baseURL, url.PathEscape(cluster.ID)) - body, statusCode, err := signedDelete(ctx, endpoint, creds, region) + // Delete the cluster via SDK + err = cs.HyperfleetV1alpha1().Clusters().Delete(ctx, clusterID, platform.DeleteOptions{}) if err != nil { - return fmt.Errorf("delete request failed: %w", err) + if k8serrors.IsNotFound(err) { + return fmt.Errorf("cluster %q not found (may have already been deleted)", nameOrID) + } + return fmt.Errorf("failed to delete cluster: %w", err) } - switch statusCode { - case http.StatusAccepted: - fmt.Fprintf(os.Stderr, "Cluster %q (ID: %s) deletion initiated.\n", cluster.Name, cluster.ID) - case http.StatusNotFound: - return fmt.Errorf("cluster %q not found (may have already been deleted)", nameOrID) - default: - return fmt.Errorf("API request failed with status %d: %s", statusCode, string(body)) - } + fmt.Fprintf(os.Stderr, "Cluster %q (ID: %s) deletion initiated.\n", clusterName, clusterID) if !opts.wait { return nil } - fmt.Fprintf(os.Stderr, "Waiting for cluster %q to be deleted...\n", cluster.Name) + fmt.Fprintf(os.Stderr, "Waiting for cluster %q to be deleted...\n", clusterName) const ( pollInterval = 15 * time.Second timeout = 10 * time.Minute ) - deadline := time.Now().Add(timeout) - checkEndpoint := fmt.Sprintf("%s/api/v0/clusters/%s", baseURL, url.PathEscape(cluster.ID)) - for time.Now().Before(deadline) { - time.Sleep(pollInterval) - _, err := signedGet(ctx, checkEndpoint, creds, region) - if err != nil { - // signedGet returns an error for non-200; a 404/410 means deletion is complete - if strings.Contains(err.Error(), "404") || strings.Contains(err.Error(), "410") { - fmt.Fprintf(os.Stderr, "Cluster %q deleted successfully.\n", cluster.Name) - return nil - } - // Transient error — keep polling - fmt.Fprintf(os.Stderr, "Polling cluster status (transient error): %v\n", err) - continue + // Use the SDK's WaitUntil to poll for deletion + err = cs.HyperfleetV1alpha1().Clusters().WaitUntil(ctx, clusterID, func(c *v1alpha1.Cluster) bool { + // Cluster is deleted when it's not found (c == nil) + return c == nil + }, pollInterval, timeout) + + if err != nil { + if err == context.DeadlineExceeded { + return fmt.Errorf("timed out waiting for cluster %q to be deleted after %s", clusterName, timeout) } - fmt.Fprintf(os.Stderr, "Cluster %q still deleting...\n", cluster.Name) + return fmt.Errorf("error waiting for cluster deletion: %w", err) } - return fmt.Errorf("timed out waiting for cluster %q to be deleted after %s", cluster.Name, timeout) + fmt.Fprintf(os.Stderr, "Cluster %q deleted successfully.\n", clusterName) + return nil } diff --git a/internal/commands/cluster/helpers.go b/internal/commands/cluster/helpers.go new file mode 100644 index 0000000..8771307 --- /dev/null +++ b/internal/commands/cluster/helpers.go @@ -0,0 +1,52 @@ +package cluster + +import ( + "context" + "fmt" + + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public" + hfclient "github.com/openshift-online/rosa-hyperfleet-api/clientset" + "github.com/openshift-online/rosa-hyperfleet-api/clientset/platform" + k8serrors "k8s.io/apimachinery/pkg/api/errors" +) + +// getClusterByNameOrID fetches a cluster by ID or name. +// It first tries to get by ID (fast path), then falls back to searching by name. +func getClusterByNameOrID(ctx context.Context, cs *hfclient.Clientset, nameOrID string) (*v1alpha1.Cluster, error) { + // Try to get by ID first (fast path) + cluster, err := cs.HyperfleetV1alpha1().Clusters().Get(ctx, nameOrID, platform.GetOptions{}) + if err == nil { + return cluster, nil + } + + // If not found, search by name + if k8serrors.IsNotFound(err) { + const pageSize = 100 + for offset := 0; ; offset += pageSize { + clusterList, err := cs.HyperfleetV1alpha1().Clusters().List(ctx, platform.ListOptions{ + Limit: pageSize, + Offset: int64(offset), + }) + if err != nil { + return nil, fmt.Errorf("failed to list clusters: %w", err) + } + + // Search for matching name or ID + for _, c := range clusterList.Items { + if c.Name == nameOrID || string(c.UID) == nameOrID { + return &c, nil + } + } + + // If we got fewer items than requested, we've reached the end + if len(clusterList.Items) < pageSize { + break + } + } + + return nil, fmt.Errorf("cluster %q not found", nameOrID) + } + + // Other error + return nil, fmt.Errorf("failed to fetch cluster: %w", err) +} diff --git a/internal/commands/cluster/kubeconfig.go b/internal/commands/cluster/kubeconfig.go index 64ecbf8..b7ee60f 100644 --- a/internal/commands/cluster/kubeconfig.go +++ b/internal/commands/cluster/kubeconfig.go @@ -10,7 +10,7 @@ import ( "text/template" "github.com/openshift-online/rosa-regional-platform-cli/internal/aws" - "github.com/openshift-online/rosa-regional-platform-cli/internal/config" + platformclient "github.com/openshift-online/rosa-regional-platform-cli/internal/platform" "github.com/spf13/cobra" ) @@ -46,39 +46,37 @@ for AWS IAM authentication. Pipe the output to a file and use with kubectl: } func runKubeconfig(ctx context.Context, nameOrID string) error { - baseURL, err := config.GetPlatformAPIURL() - if err != nil { - return err - } - cfg, err := aws.NewConfig(ctx) if err != nil { return fmt.Errorf("failed to load AWS config: %w", err) } - creds, err := cfg.Credentials.Retrieve(ctx) - if err != nil { - return fmt.Errorf("failed to retrieve AWS credentials: %w", err) - } - region := cfg.Region if region == "" { region = "us-east-1" } - cluster, err := fetchClusterByName(ctx, baseURL, nameOrID, creds, region) + // Create the clientset + cs, err := platformclient.NewClientset(ctx, cfg) if err != nil { - return err + return fmt.Errorf("failed to create clientset: %w", err) } - apiEndpoint, err := fetchAPIURL(ctx, baseURL, cluster.ID, creds, region) + // Fetch cluster via SDK (try by ID first, then search by name) + cluster, err := getClusterByNameOrID(ctx, cs, nameOrID) if err != nil { return err } - if apiEndpoint == "" { + + // Extract API endpoint from cluster status + if cluster.Status.ControlPlaneEndpoint.Host == "" { return fmt.Errorf("cluster %q API endpoint not available yet", nameOrID) } + apiEndpoint := fmt.Sprintf("https://%s:%d", + cluster.Status.ControlPlaneEndpoint.Host, + cluster.Status.ControlPlaneEndpoint.Port) + rosactlPath, _ := os.Executable() if rosactlPath == "" { rosactlPath = "rosactl" @@ -91,7 +89,7 @@ func runKubeconfig(ctx context.Context, nameOrID string) error { Server: apiEndpoint, ClusterName: cluster.Name, RosactlPath: rosactlPath, - ClusterID: cluster.ID, + ClusterID: string(cluster.UID), Region: region, }); err != nil { return fmt.Errorf("failed to render kubeconfig: %w", err) diff --git a/internal/commands/cluster/list.go b/internal/commands/cluster/list.go index 0c3fd9e..5fc7565 100644 --- a/internal/commands/cluster/list.go +++ b/internal/commands/cluster/list.go @@ -4,12 +4,12 @@ import ( "context" "encoding/json" "fmt" - "net/url" "os" "text/tabwriter" + "github.com/openshift-online/rosa-hyperfleet-api/clientset/platform" "github.com/openshift-online/rosa-regional-platform-cli/internal/aws" - "github.com/openshift-online/rosa-regional-platform-cli/internal/config" + platformclient "github.com/openshift-online/rosa-regional-platform-cli/internal/platform" "github.com/spf13/cobra" ) @@ -44,13 +44,6 @@ type clusterItem struct { Status clusterStatus `json:"status"` } -type listResponse struct { - Items []clusterItem `json:"items"` - Total int `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} - func newListCommand() *cobra.Command { opts := &listOptions{ limit: 50, @@ -82,53 +75,73 @@ Example: } func runList(ctx context.Context, opts *listOptions) error { - baseURL, err := config.GetPlatformAPIURL() - if err != nil { - return err - } - cfg, err := aws.NewConfig(ctx) if err != nil { return fmt.Errorf("failed to load AWS config: %w", err) } - creds, err := cfg.Credentials.Retrieve(ctx) - if err != nil { - return fmt.Errorf("failed to retrieve AWS credentials: %w", err) - } - - region := cfg.Region - if region == "" { - region = "us-east-1" + if cfg.Region == "" { + cfg.Region = "us-east-1" } - endpoint := fmt.Sprintf("%s/api/v0/clusters?limit=%d&offset=%d", baseURL, opts.limit, opts.offset) - if opts.status != "" { - endpoint = fmt.Sprintf("%s&status=%s", endpoint, url.QueryEscape(opts.status)) + // Create the clientset + cs, err := platformclient.NewClientset(ctx, cfg) + if err != nil { + return fmt.Errorf("failed to create clientset: %w", err) } - body, err := signedGet(ctx, endpoint, creds, region) + // List clusters via SDK + clusterList, err := cs.HyperfleetV1alpha1().Clusters().List(ctx, platform.ListOptions{ + Limit: int64(opts.limit), + Offset: int64(opts.offset), + }) if err != nil { - return err + return fmt.Errorf("failed to list clusters: %w", err) } if opts.output == "json" { - var result map[string]interface{} - if err := json.Unmarshal(body, &result); err != nil { - fmt.Println(string(body)) - return nil + // Convert to JSON for output + resultBytes, err := json.MarshalIndent(clusterList, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal response: %w", err) } - prettyJSON, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(prettyJSON)) + fmt.Println(string(resultBytes)) return nil } - var result listResponse - if err := json.Unmarshal(body, &result); err != nil { - return fmt.Errorf("failed to parse response: %w", err) + // Convert typed list to clusterItem for table display + items := make([]clusterItem, 0, len(clusterList.Items)) + for _, c := range clusterList.Items { + item := clusterItem{ + ID: string(c.UID), + Name: c.Name, + Spec: clusterSpec{ + Version: c.Spec.HostedCluster.Release.Image, + }, + } + + // Extract conditions from status + if c.Status.Conditions != nil { + for _, cond := range c.Status.Conditions { + item.Status.Conditions = append(item.Status.Conditions, condition{ + Type: cond.Type, + Status: string(cond.Status), + Message: cond.Message, + }) + } + } + + // Filter by status if specified + if opts.status != "" { + if getConditionStatus(item.Status.Conditions, opts.status) == "-" { + continue + } + } + + items = append(items, item) } - return displayTable(result.Items) + return displayTable(items) } func displayTable(clusters []clusterItem) error { diff --git a/internal/commands/nodepool/api.go b/internal/commands/nodepool/api.go deleted file mode 100644 index b17c6b8..0000000 --- a/internal/commands/nodepool/api.go +++ /dev/null @@ -1,65 +0,0 @@ -package nodepool - -import ( - "bytes" - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "io" - "net/http" - "time" - - awssdk "github.com/aws/aws-sdk-go-v2/aws" - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" -) - -var httpClient = &http.Client{Timeout: 30 * time.Second} - -func signedRequest(ctx context.Context, method, url string, body []byte, creds awssdk.Credentials, region string) ([]byte, int, error) { - var bodyReader io.Reader - if body != nil { - bodyReader = bytes.NewReader(body) - } - - req, err := http.NewRequestWithContext(ctx, method, url, bodyReader) - if err != nil { - return nil, 0, fmt.Errorf("failed to create request: %w", err) - } - if body != nil { - req.Header.Set("Content-Type", "application/json") - } - - payloadHash := sha256.Sum256(body) - payloadHashStr := hex.EncodeToString(payloadHash[:]) - - signer := v4.NewSigner() - if err := signer.SignHTTP(ctx, creds, req, payloadHashStr, "execute-api", region, time.Now()); err != nil { - return nil, 0, fmt.Errorf("failed to sign request: %w", err) - } - - resp, err := httpClient.Do(req) - if err != nil { - return nil, 0, fmt.Errorf("failed to execute request: %w", err) - } - defer func() { _ = resp.Body.Close() }() - - respBody, err := io.ReadAll(resp.Body) - if err != nil { - return nil, resp.StatusCode, fmt.Errorf("failed to read response: %w", err) - } - - return respBody, resp.StatusCode, nil -} - -func signedGet(ctx context.Context, url string, creds awssdk.Credentials, region string) ([]byte, int, error) { - return signedRequest(ctx, http.MethodGet, url, nil, creds, region) -} - -func signedPost(ctx context.Context, url string, body []byte, creds awssdk.Credentials, region string) ([]byte, int, error) { - return signedRequest(ctx, http.MethodPost, url, body, creds, region) -} - -func signedDelete(ctx context.Context, url string, creds awssdk.Credentials, region string) ([]byte, int, error) { - return signedRequest(ctx, http.MethodDelete, url, nil, creds, region) -} diff --git a/internal/commands/nodepool/create.go b/internal/commands/nodepool/create.go index 5d241e1..daf4294 100644 --- a/internal/commands/nodepool/create.go +++ b/internal/commands/nodepool/create.go @@ -4,14 +4,14 @@ import ( "context" "encoding/json" "fmt" - "net/url" "os" "strings" - awssdk "github.com/aws/aws-sdk-go-v2/aws" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public" + "github.com/openshift-online/rosa-hyperfleet-api/clientset/platform" "github.com/openshift-online/rosa-regional-platform-cli/internal/aws" "github.com/openshift-online/rosa-regional-platform-cli/internal/aws/cloudformation" - "github.com/openshift-online/rosa-regional-platform-cli/internal/config" + platformclient "github.com/openshift-online/rosa-regional-platform-cli/internal/platform" "github.com/spf13/cobra" ) @@ -71,34 +71,30 @@ Examples: } func runCreate(ctx context.Context, opts *createOptions) error { - baseURL, err := config.GetPlatformAPIURL() - if err != nil { - return err - } - cfg, err := aws.NewConfig(ctx) if err != nil { return fmt.Errorf("failed to load AWS config: %w", err) } - creds, err := cfg.Credentials.Retrieve(ctx) - if err != nil { - return fmt.Errorf("failed to retrieve AWS credentials: %w", err) + if cfg.Region == "" { + return aws.ErrRegionRequired } - region := cfg.Region - if region == "" { - return aws.ErrRegionRequired + // Create the clientset + cs, err := platformclient.NewClientset(ctx, cfg) + if err != nil { + return fmt.Errorf("failed to create clientset: %w", err) } // Auto-discover infra from cluster spec and CloudFormation stacks. if opts.subnetID == "" || opts.instanceProfile == "" || opts.securityGroups == "" { - cluster, err := fetchClusterSpec(ctx, baseURL, opts.clusterID, creds, region) + cluster, err := cs.HyperfleetV1alpha1().Clusters().Get(ctx, opts.clusterID, platform.GetOptions{}) if err != nil { return fmt.Errorf("failed to fetch cluster spec for auto-discovery: %w", err) } + if opts.subnetID == "" { - opts.subnetID = extractSubnetFromClusterSpec(cluster.Spec) + opts.subnetID = extractSubnetFromClusterSpec(cluster) } if opts.instanceProfile == "" || opts.securityGroups == "" { cfnClient := cloudformation.NewClient(cfg) @@ -140,9 +136,14 @@ func runCreate(ctx context.Context, opts *createOptions) error { } } + // Build the payload with Kubernetes-style structure payload := map[string]interface{}{ - "cluster_id": opts.clusterID, - "name": opts.name, + "apiVersion": "hyperfleet.io/v1alpha1", + "kind": "NodePool", + "metadata": map[string]interface{}{ + "name": opts.name, + "namespace": opts.clusterID, // NodePools are namespaced by cluster ID + }, "spec": map[string]interface{}{ "nodePool": map[string]interface{}{ "replicas": opts.replicas, @@ -159,24 +160,33 @@ func runCreate(ctx context.Context, opts *createOptions) error { }, } + // Convert map to typed NodePool via JSON marshal/unmarshal payloadBytes, err := json.Marshal(payload) if err != nil { return fmt.Errorf("failed to marshal request: %w", err) } - endpoint := fmt.Sprintf("%s/api/v0/nodepools", baseURL) - body, statusCode, err := signedPost(ctx, endpoint, payloadBytes, creds, region) + var nodepool v1alpha1.NodePool + if err := json.Unmarshal(payloadBytes, &nodepool); err != nil { + return fmt.Errorf("failed to unmarshal into NodePool type: %w", err) + } + + // Create the nodepool via SDK + // Note: NodePools are namespaced by cluster ID + created, err := cs.HyperfleetV1alpha1().NodePools(opts.clusterID).Create(ctx, &nodepool, platform.CreateOptions{}) if err != nil { - return err + return fmt.Errorf("failed to create nodepool via SDK: %w", err) } - if statusCode != 201 { - return fmt.Errorf("API request failed with status %d: %s", statusCode, string(body)) + // Convert response to map for display compatibility + responseBytes, err := json.Marshal(created) + if err != nil { + return fmt.Errorf("failed to marshal response: %w", err) } var result map[string]interface{} - if err := json.Unmarshal(body, &result); err != nil { - return fmt.Errorf("failed to parse response: %w", err) + if err := json.Unmarshal(responseBytes, &result); err != nil { + return fmt.Errorf("failed to unmarshal response: %w", err) } if opts.output == "json" { @@ -187,10 +197,8 @@ func runCreate(ctx context.Context, opts *createOptions) error { fmt.Fprintf(os.Stderr, "\n✓ NodePool created successfully\n") fmt.Fprintf(os.Stderr, "\nNodePool Details:\n") - fmt.Fprintf(os.Stderr, " Name: %s\n", opts.name) - if id, ok := result["id"].(string); ok { - fmt.Fprintf(os.Stderr, " ID: %s\n", id) - } + fmt.Fprintf(os.Stderr, " Name: %s\n", created.Name) + fmt.Fprintf(os.Stderr, " ID: %s\n", string(created.UID)) fmt.Fprintf(os.Stderr, " Cluster: %s\n", opts.clusterID) fmt.Fprintf(os.Stderr, " Replicas: %d\n", opts.replicas) fmt.Fprintf(os.Stderr, " Instance Type: %s\n", opts.instanceType) @@ -198,50 +206,20 @@ func runCreate(ctx context.Context, opts *createOptions) error { return nil } -type clusterResponse struct { - ID string `json:"id"` - Name string `json:"name"` - Spec map[string]interface{} `json:"spec"` -} - -func extractSubnetFromClusterSpec(spec map[string]interface{}) string { - hc, _ := spec["hostedCluster"].(map[string]interface{}) - if hc == nil { - return "" - } - platform, _ := hc["platform"].(map[string]interface{}) - if platform == nil { +func extractSubnetFromClusterSpec(cluster *v1alpha1.Cluster) string { + if cluster == nil { return "" } - awsPlat, _ := platform["aws"].(map[string]interface{}) + + awsPlat := cluster.Spec.HostedCluster.Platform.AWS if awsPlat == nil { return "" } - cpc, _ := awsPlat["cloudProviderConfig"].(map[string]interface{}) - if cpc == nil { - return "" - } - subnet, _ := cpc["subnet"].(map[string]interface{}) - if subnet == nil { - return "" - } - id, _ := subnet["id"].(string) - return id -} -func fetchClusterSpec(ctx context.Context, baseURL, clusterID string, creds awssdk.Credentials, region string) (*clusterResponse, error) { - endpoint := fmt.Sprintf("%s/api/v0/clusters/%s", baseURL, url.PathEscape(clusterID)) - body, statusCode, err := signedGet(ctx, endpoint, creds, region) - if err != nil { - return nil, err - } - if statusCode != 200 { - return nil, fmt.Errorf("failed to get cluster %s: status %d: %s", clusterID, statusCode, string(body)) + cpc := awsPlat.CloudProviderConfig + if cpc == nil || cpc.Subnet == nil || cpc.Subnet.ID == nil { + return "" } - var cluster clusterResponse - if err := json.Unmarshal(body, &cluster); err != nil { - return nil, fmt.Errorf("failed to parse cluster response: %w", err) - } - return &cluster, nil + return *cpc.Subnet.ID } diff --git a/internal/commands/nodepool/create_test.go b/internal/commands/nodepool/create_test.go new file mode 100644 index 0000000..11c6228 --- /dev/null +++ b/internal/commands/nodepool/create_test.go @@ -0,0 +1,146 @@ +package nodepool + +import ( + "testing" + + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public" + hypershiftv1beta1 "github.com/openshift/hypershift/api/hypershift/v1beta1" +) + +func TestExtractSubnetFromClusterSpec(t *testing.T) { + tests := []struct { + name string + cluster *v1alpha1.Cluster + expected string + }{ + { + name: "nil cluster", + cluster: nil, + expected: "", + }, + { + name: "cluster with no AWS platform", + cluster: &v1alpha1.Cluster{ + Spec: v1alpha1.ClusterSpec{ + HostedCluster: v1alpha1.HostedClusterSpecPassthrough{ + Platform: hypershiftv1beta1.PlatformSpec{ + Type: hypershiftv1beta1.AWSPlatform, + }, + }, + }, + }, + expected: "", + }, + { + name: "cluster with subnet ID", + cluster: &v1alpha1.Cluster{ + Spec: v1alpha1.ClusterSpec{ + HostedCluster: v1alpha1.HostedClusterSpecPassthrough{ + Platform: hypershiftv1beta1.PlatformSpec{ + Type: hypershiftv1beta1.AWSPlatform, + AWS: &hypershiftv1beta1.AWSPlatformSpec{ + CloudProviderConfig: &hypershiftv1beta1.AWSCloudProviderConfig{ + Subnet: &hypershiftv1beta1.AWSResourceReference{ + ID: strPtr("subnet-12345"), + }, + }, + }, + }, + }, + }, + }, + expected: "subnet-12345", + }, + { + name: "cluster with nil subnet", + cluster: &v1alpha1.Cluster{ + Spec: v1alpha1.ClusterSpec{ + HostedCluster: v1alpha1.HostedClusterSpecPassthrough{ + Platform: hypershiftv1beta1.PlatformSpec{ + Type: hypershiftv1beta1.AWSPlatform, + AWS: &hypershiftv1beta1.AWSPlatformSpec{ + CloudProviderConfig: &hypershiftv1beta1.AWSCloudProviderConfig{ + Subnet: nil, + }, + }, + }, + }, + }, + }, + expected: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := extractSubnetFromClusterSpec(tt.cluster) + if result != tt.expected { + t.Errorf("extractSubnetFromClusterSpec() = %q, want %q", result, tt.expected) + } + }) + } +} + +func TestCreateOptions_Validation(t *testing.T) { + tests := []struct { + name string + opts *createOptions + expectError bool + errorMsg string + }{ + { + name: "valid options", + opts: &createOptions{ + name: "test-nodepool", + clusterID: "cluster-123", + replicas: 2, + instanceType: "m5.xlarge", + subnetID: "subnet-123", + instanceProfile: "test-profile", + securityGroups: "sg-123", + }, + expectError: false, + }, + { + name: "missing cluster ID", + opts: &createOptions{ + name: "test-nodepool", + replicas: 2, + }, + expectError: true, + }, + { + name: "replicas less than 1", + opts: &createOptions{ + name: "test-nodepool", + clusterID: "cluster-123", + replicas: 0, + }, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Validate cluster ID + if tt.opts.clusterID == "" && tt.expectError { + // Expected validation failure + return + } + + // Validate replicas + if tt.opts.replicas < 1 && tt.expectError { + // Expected validation failure + return + } + + if !tt.expectError && (tt.opts.clusterID == "" || tt.opts.replicas < 1) { + t.Error("expected validation to pass, but options are invalid") + } + }) + } +} + +func strPtr(s string) *string { + return &s +} diff --git a/internal/commands/nodepool/delete.go b/internal/commands/nodepool/delete.go index 9ace09c..2e56230 100644 --- a/internal/commands/nodepool/delete.go +++ b/internal/commands/nodepool/delete.go @@ -3,60 +3,67 @@ package nodepool import ( "context" "fmt" - "net/url" "os" + "github.com/openshift-online/rosa-hyperfleet-api/clientset/platform" "github.com/openshift-online/rosa-regional-platform-cli/internal/aws" - "github.com/openshift-online/rosa-regional-platform-cli/internal/config" + platformclient "github.com/openshift-online/rosa-regional-platform-cli/internal/platform" "github.com/spf13/cobra" + k8serrors "k8s.io/apimachinery/pkg/api/errors" ) +type nodepoolDeleteOptions struct { + clusterID string +} + func newDeleteCommand() *cobra.Command { + opts := &nodepoolDeleteOptions{} + cmd := &cobra.Command{ Use: "delete NODEPOOL_ID", Short: "Delete a node pool", Long: `Delete a node pool from a ROSA hosted cluster. Examples: - rosactl nodepool delete --region us-east-1`, + rosactl nodepool delete --cluster-id --region us-east-1`, Args: cobra.ExactArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - return runDelete(cmd.Context(), args[0]) + if opts.clusterID == "" { + return fmt.Errorf("--cluster-id is required") + } + return runDelete(cmd.Context(), args[0], opts) }, } + cmd.Flags().StringVar(&opts.clusterID, "cluster-id", "", "Cluster ID (required)") + return cmd } -func runDelete(ctx context.Context, nodepoolID string) error { - baseURL, err := config.GetPlatformAPIURL() - if err != nil { - return err - } - +func runDelete(ctx context.Context, nodepoolID string, opts *nodepoolDeleteOptions) error { cfg, err := aws.NewConfig(ctx) if err != nil { return fmt.Errorf("failed to load AWS config: %w", err) } - creds, err := cfg.Credentials.Retrieve(ctx) - if err != nil { - return fmt.Errorf("failed to retrieve AWS credentials: %w", err) - } - - region := cfg.Region - if region == "" { + if cfg.Region == "" { return aws.ErrRegionRequired } - endpoint := fmt.Sprintf("%s/api/v0/nodepools/%s", baseURL, url.PathEscape(nodepoolID)) - body, statusCode, err := signedDelete(ctx, endpoint, creds, region) + // Create the clientset + cs, err := platformclient.NewClientset(ctx, cfg) if err != nil { - return err + return fmt.Errorf("failed to create clientset: %w", err) } - if statusCode != 202 { - return fmt.Errorf("API request failed with status %d: %s", statusCode, string(body)) + // Delete the nodepool via SDK + // NodePools are namespaced by cluster ID + err = cs.HyperfleetV1alpha1().NodePools(opts.clusterID).Delete(ctx, nodepoolID, platform.DeleteOptions{}) + if err != nil { + if k8serrors.IsNotFound(err) { + return fmt.Errorf("nodepool %q not found in cluster %q", nodepoolID, opts.clusterID) + } + return fmt.Errorf("failed to delete nodepool: %w", err) } fmt.Fprintf(os.Stderr, "✓ NodePool %s deletion initiated\n", nodepoolID) diff --git a/internal/commands/nodepool/list.go b/internal/commands/nodepool/list.go index 26a1f9e..5002338 100644 --- a/internal/commands/nodepool/list.go +++ b/internal/commands/nodepool/list.go @@ -4,12 +4,12 @@ import ( "context" "encoding/json" "fmt" - "net/url" "os" "text/tabwriter" + "github.com/openshift-online/rosa-hyperfleet-api/clientset/platform" "github.com/openshift-online/rosa-regional-platform-cli/internal/aws" - "github.com/openshift-online/rosa-regional-platform-cli/internal/config" + platformclient "github.com/openshift-online/rosa-regional-platform-cli/internal/platform" "github.com/spf13/cobra" ) @@ -20,25 +20,6 @@ type listOptions struct { output string } -type nodepoolItem struct { - ID string `json:"id"` - Name string `json:"name"` - ClusterID string `json:"cluster_id"` - Spec map[string]interface{} `json:"spec"` - Status *nodepoolStatus `json:"status"` -} - -type nodepoolStatus struct { - Phase string `json:"phase"` -} - -type listResponse struct { - Items []nodepoolItem `json:"items"` - Total int `json:"total"` - Limit int `json:"limit"` - Offset int `json:"offset"` -} - func newListCommand() *cobra.Command { opts := &listOptions{ limit: 50, @@ -76,80 +57,63 @@ Examples: } func runList(ctx context.Context, opts *listOptions) error { - baseURL, err := config.GetPlatformAPIURL() - if err != nil { - return err - } - cfg, err := aws.NewConfig(ctx) if err != nil { return fmt.Errorf("failed to load AWS config: %w", err) } - creds, err := cfg.Credentials.Retrieve(ctx) - if err != nil { - return fmt.Errorf("failed to retrieve AWS credentials: %w", err) - } - - region := cfg.Region - if region == "" { + if cfg.Region == "" { return aws.ErrRegionRequired } - endpoint := fmt.Sprintf("%s/api/v0/nodepools?limit=%d&offset=%d&clusterId=%s", - baseURL, opts.limit, opts.offset, url.QueryEscape(opts.clusterID)) - - body, statusCode, err := signedGet(ctx, endpoint, creds, region) + // Create the clientset + cs, err := platformclient.NewClientset(ctx, cfg) if err != nil { - return err + return fmt.Errorf("failed to create clientset: %w", err) } - if statusCode != 200 { - return fmt.Errorf("API request failed with status %d: %s", statusCode, string(body)) + + // List nodepools via SDK + // NodePools are namespaced by cluster ID + nodepoolList, err := cs.HyperfleetV1alpha1().NodePools(opts.clusterID).List(ctx, platform.ListOptions{ + Limit: int64(opts.limit), + Offset: int64(opts.offset), + }) + if err != nil { + return fmt.Errorf("failed to list nodepools: %w", err) } if opts.output == "json" { - var result map[string]interface{} - if err := json.Unmarshal(body, &result); err != nil { - return fmt.Errorf("failed to parse JSON response: %w", err) + // Convert to JSON for output + resultBytes, err := json.MarshalIndent(nodepoolList, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal response: %w", err) } - prettyJSON, _ := json.MarshalIndent(result, "", " ") - fmt.Println(string(prettyJSON)) + fmt.Println(string(resultBytes)) return nil } - var result listResponse - if err := json.Unmarshal(body, &result); err != nil { - return fmt.Errorf("failed to parse response: %w", err) - } - w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) if _, err := fmt.Fprintln(w, "ID\tNAME\tREPLICAS\tINSTANCE_TYPE\tPHASE"); err != nil { return err } - for _, np := range result.Items { + for _, np := range nodepoolList.Items { replicas := "-" instanceType := "-" phase := "-" - if spec := np.Spec; spec != nil { - if r, ok := spec["replicas"].(float64); ok { - replicas = fmt.Sprintf("%.0f", r) - } - if p, ok := spec["platform"].(map[string]interface{}); ok { - if a, ok := p["aws"].(map[string]interface{}); ok { - if it, ok := a["instanceType"].(string); ok { - instanceType = it - } - } - } + if np.Spec.NodePool.Replicas != nil { + replicas = fmt.Sprintf("%d", *np.Spec.NodePool.Replicas) + } + if np.Spec.NodePool.Platform.AWS != nil && np.Spec.NodePool.Platform.AWS.InstanceType != "" { + instanceType = np.Spec.NodePool.Platform.AWS.InstanceType } - if np.Status != nil && np.Status.Phase != "" { - phase = np.Status.Phase + if np.Status.Phase != "" { + phase = string(np.Status.Phase) } if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", - np.ID, np.Name, replicas, instanceType, phase); err != nil { + string(np.UID), np.Name, replicas, instanceType, phase); err != nil { return err } } diff --git a/internal/platform/client.go b/internal/platform/client.go new file mode 100644 index 0000000..2fcf94c --- /dev/null +++ b/internal/platform/client.go @@ -0,0 +1,49 @@ +package platform + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/sts" + hfclient "github.com/openshift-online/rosa-hyperfleet-api/clientset" + hfrest "github.com/openshift-online/rosa-hyperfleet-api/clientset/rest" + pkgconfig "github.com/openshift-online/rosa-regional-platform-cli/internal/config" +) + +// NewClientset creates a Hyperfleet platform API clientset from AWS config. +// It resolves the platform API URL from config and derives the AWS account ID +// from STS GetCallerIdentity. +func NewClientset(ctx context.Context, awsCfg aws.Config) (*hfclient.Clientset, error) { + baseURL, err := pkgconfig.GetPlatformAPIURL() + if err != nil { + return nil, fmt.Errorf("failed to get platform API URL: %w", err) + } + + accountID, err := getAWSAccountID(ctx, awsCfg) + if err != nil { + return nil, fmt.Errorf("failed to get AWS account ID: %w", err) + } + + cfg := &hfrest.Config{ + Host: baseURL, + AccountID: accountID, + AWSConfig: awsCfg, + } + + cs, err := hfclient.NewForConfig(cfg) + if err != nil { + return nil, fmt.Errorf("failed to create clientset: %w", err) + } + + return cs, nil +} + +func getAWSAccountID(ctx context.Context, cfg aws.Config) (string, error) { + stsClient := sts.NewFromConfig(cfg) + identity, err := stsClient.GetCallerIdentity(ctx, &sts.GetCallerIdentityInput{}) + if err != nil { + return "", err + } + return aws.ToString(identity.Account), nil +} diff --git a/internal/platform/client_test.go b/internal/platform/client_test.go new file mode 100644 index 0000000..ee9ec52 --- /dev/null +++ b/internal/platform/client_test.go @@ -0,0 +1,83 @@ +package platform + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/credentials" +) + +func TestNewClientset_RequiresPlatformURL(t *testing.T) { + t.Setenv("ROSA_PLATFORM_API_URL", "") + + ctx := context.Background() + cfg := aws.Config{ + Region: "us-east-1", + Credentials: credentials.NewStaticCredentialsProvider( + "test-access-key", + "test-secret-key", + "", + ), + } + + _, err := NewClientset(ctx, cfg) + if err == nil { + t.Fatal("NewClientset() expected error when platform URL not set, got nil") + } + // The error could be about missing platform URL or about AWS credentials + // depending on whether the env var check happens first + expectedErrors := []string{ + "failed to get platform API URL: ROSA_PLATFORM_API_URL environment variable is not set", + "failed to get AWS account ID", + } + + matched := false + for _, expected := range expectedErrors { + if stringContains(err.Error(), expected) { + matched = true + break + } + } + + if !matched { + t.Errorf("unexpected error message: %v", err) + } +} + +func stringContains(s, substr string) bool { + return len(s) >= len(substr) && (s == substr || indexOfString(s, substr) >= 0) +} + +func indexOfString(s, substr string) int { + for i := 0; i <= len(s)-len(substr); i++ { + if s[i:i+len(substr)] == substr { + return i + } + } + return -1 +} + +func TestNewClientset_RequiresValidURL(t *testing.T) { + t.Setenv("ROSA_PLATFORM_API_URL", "https://example.execute-api.us-east-1.amazonaws.com") + + ctx := context.Background() + cfg := aws.Config{ + Region: "us-east-1", + Credentials: credentials.NewStaticCredentialsProvider( + "test-access-key", + "test-secret-key", + "", + ), + } + + // This will fail at STS GetCallerIdentity since we're using fake credentials + _, err := NewClientset(ctx, cfg) + if err == nil { + t.Fatal("NewClientset() expected error with fake credentials, got nil") + } + // Should fail at getting AWS account ID + if !stringContains(err.Error(), "failed to get AWS account ID") { + t.Errorf("expected error to contain 'failed to get AWS account ID', got: %v", err) + } +} diff --git a/internal/services/cluster/service.go b/internal/services/cluster/service.go index 1e57dc3..dd6cf67 100644 --- a/internal/services/cluster/service.go +++ b/internal/services/cluster/service.go @@ -1,23 +1,19 @@ package cluster import ( - "bytes" "context" - "crypto/sha256" - "encoding/hex" "encoding/json" "errors" "fmt" - "io" - "net/http" "strings" - "time" "unicode" "github.com/aws/aws-sdk-go-v2/aws" - v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4" "github.com/aws/aws-sdk-go-v2/service/sts" + v1alpha1 "github.com/openshift-online/rosa-hyperfleet-api/api/v1alpha1/public" + "github.com/openshift-online/rosa-hyperfleet-api/clientset/platform" "github.com/openshift-online/rosa-regional-platform-cli/internal/aws/cloudformation" + platformclient "github.com/openshift-online/rosa-regional-platform-cli/internal/platform" ) // GenerateClusterConfigRequest contains parameters for generating cluster configuration @@ -124,19 +120,21 @@ func GenerateClusterConfig(ctx context.Context, req *GenerateClusterConfigReques } // Build labels - labels := map[string]interface{}{ + labels := map[string]string{ "environment": req.LabelEnvironment, "team": req.LabelTeam, "region": req.Region, } - // Build the cluster object + // Build the cluster object with Kubernetes-style structure clusterObj := map[string]interface{}{ - "kind": "Cluster", - "name": req.ClusterName, - "target_project_id": req.TargetProjectID, - "labels": labels, - "spec": spec, + "apiVersion": "hyperfleet.io/v1alpha1", + "kind": "Cluster", + "metadata": map[string]interface{}{ + "name": req.ClusterName, + "labels": labels, + }, + "spec": spec, } return &GenerateClusterConfigResponse{ @@ -196,8 +194,14 @@ func computeIAMRoleARNs(clusterName, accountID string) map[string]string { } } -// SubmitCluster submits a cluster configuration to the platform API +// SubmitCluster submits a cluster configuration to the platform API using the clientset SDK func SubmitCluster(ctx context.Context, req *SubmitClusterRequest) (*SubmitClusterResponse, error) { + // Create the clientset + cs, err := platformclient.NewClientset(ctx, req.AWSConfig) + if err != nil { + return nil, fmt.Errorf("failed to create clientset: %w", err) + } + // Make a copy of the payload to avoid modifying the original payload := make(map[string]interface{}) for k, v := range req.Payload { @@ -211,71 +215,33 @@ func SubmitCluster(ctx context.Context, req *SubmitClusterRequest) (*SubmitClust } } - // Marshal payload to JSON + // Convert the map payload to a typed Cluster object + // Marshal to JSON first, then unmarshal into the typed struct payloadBytes, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("failed to marshal payload: %w", err) } - // Build the API endpoint URL - endpoint := fmt.Sprintf("%s/api/v0/clusters", req.PlatformAPIURL) - - // Create HTTP request - httpReq, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewReader(payloadBytes)) - if err != nil { - return nil, fmt.Errorf("failed to create request: %w", err) - } - - httpReq.Header.Set("Content-Type", "application/json") - - // Calculate SHA256 hash of the request body - hash := sha256.Sum256(payloadBytes) - payloadHash := hex.EncodeToString(hash[:]) - - // Sign the request with AWS SigV4 - signer := v4.NewSigner() - creds, err := req.AWSConfig.Credentials.Retrieve(ctx) - if err != nil { - return nil, fmt.Errorf("failed to retrieve AWS credentials: %w", err) - } - - // Determine the region from AWS config - region := req.AWSConfig.Region - if region == "" { - region = "us-east-1" // Default region - } - - err = signer.SignHTTP(ctx, creds, httpReq, payloadHash, "execute-api", region, time.Now()) - if err != nil { - return nil, fmt.Errorf("failed to sign request: %w", err) - } - - // Execute the request - client := &http.Client{ - Timeout: 30 * time.Second, + var cluster v1alpha1.Cluster + if err := json.Unmarshal(payloadBytes, &cluster); err != nil { + return nil, fmt.Errorf("failed to unmarshal into Cluster type: %w", err) } - resp, err := client.Do(httpReq) + // Create the cluster via the SDK + created, err := cs.HyperfleetV1alpha1().Clusters().Create(ctx, &cluster, platform.CreateOptions{}) if err != nil { - return nil, fmt.Errorf("failed to execute request: %w", err) + return nil, fmt.Errorf("failed to create cluster via SDK: %w", err) } - defer func() { _ = resp.Body.Close() }() - // Read the response body - body, err := io.ReadAll(resp.Body) + // Convert the response back to map[string]interface{} for compatibility + responseBytes, err := json.Marshal(created) if err != nil { - return nil, fmt.Errorf("failed to read response: %w", err) - } - - // Check for error responses - if resp.StatusCode < 200 || resp.StatusCode >= 300 { - return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body)) + return nil, fmt.Errorf("failed to marshal response: %w", err) } - // Parse the JSON response var result map[string]interface{} - if err := json.Unmarshal(body, &result); err != nil { - return nil, fmt.Errorf("failed to parse response: %w", err) + if err := json.Unmarshal(responseBytes, &result); err != nil { + return nil, fmt.Errorf("failed to unmarshal response: %w", err) } return &SubmitClusterResponse{ diff --git a/internal/services/cluster/submit_test.go b/internal/services/cluster/submit_test.go new file mode 100644 index 0000000..f18f2ac --- /dev/null +++ b/internal/services/cluster/submit_test.go @@ -0,0 +1,162 @@ +package cluster + +import ( + "context" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" +) + +func TestSubmitCluster_PayloadConversion(t *testing.T) { + // This test verifies the payload structure that SubmitCluster expects + // Note: Full end-to-end testing would require refactoring SubmitCluster + // to accept an injectable clientset instead of creating one internally. + + // Build a test payload (similar to what the CLI creates) + // Uses Kubernetes-style structure with metadata object + payload := map[string]interface{}{ + "apiVersion": "hyperfleet.io/v1alpha1", + "kind": "Cluster", + "metadata": map[string]interface{}{ + "name": "test-cluster", + "labels": map[string]string{ + "team": "platform", + "environment": "dev", + }, + }, + "spec": map[string]interface{}{ + "hostedCluster": map[string]interface{}{ + "release": map[string]interface{}{ + "image": "4.22", + }, + "platform": map[string]interface{}{ + "type": "AWS", + "aws": map[string]interface{}{ + "region": "us-east-1", + }, + }, + }, + }, + } + + // Test the payload structure + t.Run("payload structure", func(t *testing.T) { + if payload["apiVersion"] != "hyperfleet.io/v1alpha1" { + t.Errorf("expected apiVersion hyperfleet.io/v1alpha1, got %v", payload["apiVersion"]) + } + if metadata, ok := payload["metadata"].(map[string]interface{}); ok { + if metadata["name"] != "test-cluster" { + t.Errorf("expected cluster name test-cluster, got %v", metadata["name"]) + } + } else { + t.Error("expected metadata object") + } + if spec, ok := payload["spec"].(map[string]interface{}); ok { + if hc, ok := spec["hostedCluster"].(map[string]interface{}); ok { + if release, ok := hc["release"].(map[string]interface{}); ok { + if release["image"] != "4.22" { + t.Errorf("expected release image 4.22, got %v", release["image"]) + } + } + } + } + }) +} + +func TestSubmitCluster_OverridesPlacement(t *testing.T) { + // Test that placement override is properly applied + payload := map[string]interface{}{ + "apiVersion": "hyperfleet.io/v1alpha1", + "kind": "Cluster", + "metadata": map[string]interface{}{ + "name": "test-cluster", + }, + "spec": map[string]interface{}{ + "placement": "original-placement", + }, + } + + placementOverride := "new-placement" + + // Apply override logic + if spec, ok := payload["spec"].(map[string]interface{}); ok { + spec["placement"] = placementOverride + } + + // Verify override was applied + if spec, ok := payload["spec"].(map[string]interface{}); ok { + if spec["placement"] != placementOverride { + t.Errorf("expected placement %s, got %v", placementOverride, spec["placement"]) + } + } else { + t.Fatal("spec not found in payload") + } +} + +func TestComputeIAMRoleARNs_GeneratesCorrectARNs(t *testing.T) { + arns := computeIAMRoleARNs("my-cluster", "123456789012") + + tests := []struct { + key string + expected string + }{ + {"IngressRoleArn", "arn:aws:iam::123456789012:role/my-cluster-ingress"}, + {"CloudControllerManagerRoleArn", "arn:aws:iam::123456789012:role/my-cluster-cloud-controller-manager"}, + {"EBSCSIRoleArn", "arn:aws:iam::123456789012:role/my-cluster-ebs-csi"}, + {"ImageRegistryRoleArn", "arn:aws:iam::123456789012:role/my-cluster-image-registry"}, + {"NetworkConfigRoleArn", "arn:aws:iam::123456789012:role/my-cluster-network-config"}, + {"ControlPlaneOperatorRoleArn", "arn:aws:iam::123456789012:role/my-cluster-control-plane-operator"}, + {"NodePoolManagementRoleArn", "arn:aws:iam::123456789012:role/my-cluster-node-pool-management"}, + {"WorkerRoleArn", "arn:aws:iam::123456789012:role/my-cluster-ROSA-Worker-Role"}, + {"WorkerInstanceProfileName", "my-cluster-ROSA-Worker-Role"}, + } + + for _, tt := range tests { + t.Run(tt.key, func(t *testing.T) { + got, ok := arns[tt.key] + if !ok { + t.Fatalf("missing key %q in arns map", tt.key) + } + if got != tt.expected { + t.Errorf("arns[%q] = %q, want %q", tt.key, got, tt.expected) + } + }) + } +} + +func TestGenerateClusterConfig_BuildsValidSpec(t *testing.T) { + ctx := context.Background() + + // This is a minimal test to verify the structure + // Full testing would require mocking CloudFormation + req := &GenerateClusterConfigRequest{ + ClusterName: "test-cluster", + Region: "us-east-1", + Version: "4.22", + ComputeReplicas: 3, + ComputeMachineType: "m5.xlarge", + Provider: "aws", + MultiAZ: true, + LabelEnvironment: "dev", + LabelTeam: "platform", + AWSConfig: aws.Config{}, + } + + // Note: This would fail without proper AWS/CFN setup + // but demonstrates the test structure + _ = req + _ = ctx + + t.Run("structure validation", func(t *testing.T) { + // Test the expected structure of the request + if req.ClusterName != "test-cluster" { + t.Errorf("expected cluster name test-cluster, got %s", req.ClusterName) + } + if req.Region != "us-east-1" { + t.Errorf("expected region us-east-1, got %s", req.Region) + } + if req.ComputeReplicas != 3 { + t.Errorf("expected 3 replicas, got %d", req.ComputeReplicas) + } + }) +}