diff --git a/components/api-server/cmd/hypershell/main.go b/components/api-server/cmd/hypershell/main.go index f2b99b98..cf65ae5c 100644 --- a/components/api-server/cmd/hypershell/main.go +++ b/components/api-server/cmd/hypershell/main.go @@ -13,6 +13,7 @@ import ( _ "github.com/openshift-online/hypershell/components/api-server/cmd/hypershell/environments" _ "github.com/openshift-online/hypershell/components/api-server/plugins/gatewayNetworks" + _ "github.com/openshift-online/hypershell/components/api-server/plugins/gatewayProfiles" _ "github.com/openshift-online/hypershell/components/api-server/plugins/gatewayReleases" _ "github.com/openshift-online/hypershell/components/api-server/plugins/gateways" _ "github.com/openshift-online/hypershell/components/api-server/plugins/managedClusters" diff --git a/components/api-server/go.mod b/components/api-server/go.mod index 9c7cd0fe..7945d0e6 100644 --- a/components/api-server/go.mod +++ b/components/api-server/go.mod @@ -30,6 +30,7 @@ require ( gopkg.in/resty.v1 v1.12.0 gopkg.in/yaml.v3 v3.0.1 gorm.io/gorm v1.31.1 + k8s.io/apimachinery v0.31.0 ) require ( @@ -56,10 +57,12 @@ require ( github.com/docker/go-healthcheck v0.1.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect + github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect @@ -103,6 +106,7 @@ require ( github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/numcpus v0.6.1 // indirect github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 // indirect + github.com/x448/float16 v0.8.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect @@ -114,6 +118,7 @@ require ( golang.org/x/text v0.40.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260720211330-0afa2a65878a // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260720211330-0afa2a65878a // indirect + gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gorm.io/driver/postgres v1.6.0 // indirect ) diff --git a/components/api-server/go.sum b/components/api-server/go.sum index 8515e7df..0f61f3ed 100644 --- a/components/api-server/go.sum +++ b/components/api-server/go.sum @@ -53,8 +53,9 @@ github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwc github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 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/denisenkom/go-mssqldb v0.0.0-20190423183735-731ef375ac02/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= github.com/denisenkom/go-mssqldb v0.0.0-20190515213511-eb9f6a1743f3/go.mod h1:zAg7JM8CkOJ43xKXIj7eRO9kmWm/TW578qo+oDO6tuM= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= @@ -77,6 +78,8 @@ github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5Kwzbycv github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= +github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-gormigrate/gormigrate/v2 v2.1.6 h1:VtX+l1Stj2v5RGubVQk0LS/8EPGXR+ldcOyCmlmKoyg= @@ -93,8 +96,12 @@ github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiU github.com/go-sql-driver/mysql v1.4.1 h1:g24URVg0OFbNUTx9qqY1IRZ9D9z3iPyi5zKhQZpNwpA= github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +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/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= @@ -119,9 +126,13 @@ github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 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= @@ -159,6 +170,7 @@ github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= @@ -221,8 +233,8 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.10.1 h1:q/mM8GF/n0shIN8SaAZ0V+jnLPzen6WIVZdiwrRlMlo= github.com/onsi/ginkgo v1.10.1/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo/v2 v2.8.1 h1:xFTEVwOFa1D/Ty24Ws1npBWkDYEV9BqZrsDxVrVkrrU= -github.com/onsi/ginkgo/v2 v2.8.1/go.mod h1:N1/NbDngAFcSLdyZ+/aYTYGSlq9qMCS/cNKGJjy+csc= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.27.1 h1:rfztXRbg6nv/5f+Raen9RcGoSecHIFgBBLQK3Wdj754= @@ -239,8 +251,9 @@ github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -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/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -310,11 +323,15 @@ github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2 h1:Jjn3zoRz13f8b1bR6Lr github.com/uptrace/opentelemetry-go-extra/otelgorm v0.3.2/go.mod h1:wocb5pNrj/sjhWB9J5jctnC0K2eisSdz/nJJBNFHo+A= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2 h1:ZjUj9BLYf9PEqBn8W/OapxhPjVRdC6CsXTdULHsyk5c= github.com/uptrace/opentelemetry-go-extra/otelsql v0.3.2/go.mod h1:O8bHQfyinKwTXKkiKNGmLQS7vRsqRxIQTFZpYpHK3IQ= +github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= +github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg/scram v0.0.0-20180814205039-7eeb5667e42c/go.mod h1:lB8K/P019DLNhemzwFU4jHLhdvlE6uDZjXFejJXr49I= github.com/xdg/stringprep v1.0.0/go.mod h1:Jhud4/sHMO4oL310DaZAKk9ZaJ08SJfe+sJh0HrGL1Y= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= github.com/yaacov/tree-search-language v0.0.0-20190923184055-1c2dad2e354b h1:aWR0+NlUGQpFPxpjcYW7oXsN1GnYUVIdB5Act7I6jzc= github.com/yaacov/tree-search-language v0.0.0-20190923184055-1c2dad2e354b/go.mod h1:uXZEzDS1siuQsBuHL1A4gy27xIsnnL06MhqrwvySsIk= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= @@ -356,6 +373,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -372,6 +391,8 @@ golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU golang.org/x/mobile v0.0.0-20190509164839-32b2708ab171/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= +golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -384,6 +405,9 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -394,6 +418,8 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -409,6 +435,7 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -422,12 +449,13 @@ golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -438,7 +466,15 @@ golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190521203540-521d6ed310dd/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190601110225-0abef6e9ecb8/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= +golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q= +golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= @@ -473,6 +509,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntN gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= +gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/resty.v1 v1.12.0 h1:CuXP0Pjfw9rOuY6EP+UvtNvt5DSqHpIxILZKT/quCZI= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= @@ -493,3 +531,5 @@ honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190531162725-42df64e2171a/go.mod h1:wtc9q0E9zm8PjdRMh29DPlTlCCHVzKDwnkT4GskQVzg= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= diff --git a/components/api-server/openapi/openapi.gatewayProfiles.yaml b/components/api-server/openapi/openapi.gatewayProfiles.yaml new file mode 100644 index 00000000..ba13045a --- /dev/null +++ b/components/api-server/openapi/openapi.gatewayProfiles.yaml @@ -0,0 +1,359 @@ +paths: + # NEW ENDPOINT START + /api/hypershell/v1/gateway_profiles: + # NEW ENDPOINT END + get: + operationId: listGatewayProfiles + summary: Returns a list of gatewayProfiles + security: + - Bearer: [] + responses: + '200': + description: A JSON array of gatewayProfile objects + content: + application/json: + schema: + $ref: '#/components/schemas/GatewayProfileList' + '401': + description: Auth token is invalid + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '403': + description: Unauthorized to perform operation + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '500': + description: Unexpected error occurred + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + parameters: + - $ref: '#/components/parameters/page' + - $ref: '#/components/parameters/size' + - $ref: '#/components/parameters/search' + - $ref: '#/components/parameters/orderBy' + - $ref: '#/components/parameters/fields' + post: + operationId: createGatewayProfile + summary: Create a new gatewayProfile + security: + - Bearer: [] + requestBody: + description: GatewayProfile data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GatewayProfile' + responses: + '201': + description: Created + content: + application/json: + schema: + $ref: '#/components/schemas/GatewayProfile' + '400': + description: Validation errors occurred + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '401': + description: Auth token is invalid + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '403': + description: Unauthorized to perform operation + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '409': + description: GatewayProfile already exists + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '500': + description: An unexpected error occurred creating the gatewayProfile + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + # NEW ENDPOINT START + /api/hypershell/v1/gateway_profiles/{id}: + # NEW ENDPOINT END + get: + operationId: getGatewayProfile + summary: Get a gatewayProfile by id + security: + - Bearer: [] + responses: + '200': + description: GatewayProfile found by id + content: + application/json: + schema: + $ref: '#/components/schemas/GatewayProfile' + '401': + description: Auth token is invalid + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '403': + description: Unauthorized to perform operation + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '404': + description: No gatewayProfile with specified id exists + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '500': + description: Unexpected error occurred + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + patch: + operationId: updateGatewayProfile + summary: Update a gatewayProfile + security: + - Bearer: [] + requestBody: + description: Updated gatewayProfile data + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/GatewayProfilePatchRequest' + responses: + '200': + description: GatewayProfile updated successfully + content: + application/json: + schema: + $ref: '#/components/schemas/GatewayProfile' + '400': + description: Validation errors occurred + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '401': + description: Auth token is invalid + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '403': + description: Unauthorized to perform operation + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '404': + description: No gatewayProfile with specified id exists + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '500': + description: Unexpected error updating gatewayProfile + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + delete: + operationId: deleteGatewayProfile + summary: Delete a gateway profile + security: + - Bearer: [] + responses: + '204': + description: Gateway profile deleted successfully + '401': + description: Auth token is invalid + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '403': + description: Unauthorized to perform operation + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '404': + description: No gateway profile with specified id exists + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '409': + description: GatewayProfile is referenced by a cluster or gateway and cannot be deleted + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + '500': + description: Unexpected error deleting gateway profile + content: + application/json: + schema: + $ref: 'openapi.yaml#/components/schemas/Error' + parameters: + - $ref: '#/components/parameters/id' +components: + schemas: + # NEW SCHEMA START + GatewayProfile: + # NEW SCHEMA END + allOf: + - $ref: 'openapi.yaml#/components/schemas/ObjectReference' + - type: object + required: + - name + properties: + name: + type: string + description: + type: string + description: Profile purpose/intent + cpu_request_total: + type: string + description: Total CPU requests allowed (e.g., "4", "500m") + cpu_limit_total: + type: string + description: Total CPU limits allowed + memory_request_total: + type: string + description: Total memory requests allowed (e.g., "8Gi", "512Mi") + memory_limit_total: + type: string + description: Total memory limits allowed + ephemeral_storage_total: + type: string + description: Total ephemeral storage allowed (e.g., "10Gi") + pod_count: + type: integer + format: int32 + description: Maximum number of pods (0 means not set) + pvc_count: + type: integer + format: int32 + description: Maximum number of PersistentVolumeClaims (0 means not set) + container_cpu_request_default: + type: string + description: Default CPU request injected into containers without explicit request + container_cpu_limit_max: + type: string + description: Maximum CPU limit a container can request + container_memory_request_default: + type: string + description: Default memory request injected into containers + container_memory_limit_max: + type: string + description: Maximum memory limit a container can request + # NEW SCHEMA START + GatewayProfileList: + # NEW SCHEMA END + allOf: + - $ref: 'openapi.yaml#/components/schemas/List' + - type: object + properties: + items: + type: array + items: + $ref: '#/components/schemas/GatewayProfile' + # NEW SCHEMA START + GatewayProfilePatchRequest: + # NEW SCHEMA END + type: object + properties: + name: + type: string + description: + type: string + cpu_request_total: + type: string + cpu_limit_total: + type: string + memory_request_total: + type: string + memory_limit_total: + type: string + ephemeral_storage_total: + type: string + pod_count: + type: integer + format: int32 + pvc_count: + type: integer + format: int32 + container_cpu_request_default: + type: string + container_cpu_limit_max: + type: string + container_memory_request_default: + type: string + container_memory_limit_max: + type: string + parameters: + id: + name: id + in: path + description: The id of record + required: true + schema: + type: string + page: + name: page + in: query + description: Page number of record list when record list exceeds specified page size + schema: + type: integer + default: 1 + minimum: 1 + required: false + size: + name: size + in: query + description: Maximum number of records to return + schema: + type: integer + default: 100 + minimum: 0 + required: false + search: + name: search + in: query + required: false + description: Specifies the search criteria + schema: + type: string + orderBy: + name: orderBy + in: query + required: false + description: Specifies the order by criteria + schema: + type: string + fields: + name: fields + in: query + required: false + description: Supplies a comma-separated list of fields to be returned + schema: + type: string diff --git a/components/api-server/openapi/openapi.gateways.yaml b/components/api-server/openapi/openapi.gateways.yaml index e5ff4a13..a9ebd027 100644 --- a/components/api-server/openapi/openapi.gateways.yaml +++ b/components/api-server/openapi/openapi.gateways.yaml @@ -236,6 +236,9 @@ components: type: string release_id: type: string + profile_id: + type: string + description: GatewayProfile identifier used for namespace quota enforcement; required at creation (client-supplied or inherited from the cluster default) and reassignable but not clearable afterward database_id: type: string description: Server-assigned ManagedDatabase identifier; client-supplied values are ignored @@ -305,6 +308,9 @@ components: type: string release_id: type: string + profile_id: + type: string + description: Optional GatewayProfile identifier for quota enforcement; if omitted the API server falls back to the cluster's default profile, and rejects the request with HTTP 400 if neither is provided database_id: type: string description: Required placement placeholder; the API server ignores its value and assigns the ManagedDatabase @@ -360,6 +366,9 @@ components: type: string release_id: type: string + profile_id: + type: string + description: Reassign the gateway to a different GatewayProfile; cannot be set to null or empty (HTTP 400) and the target profile must exist database_id: type: string description: Server-owned placement field; values supplied through PATCH are ignored diff --git a/components/api-server/openapi/openapi.managedClusters.yaml b/components/api-server/openapi/openapi.managedClusters.yaml index ecc6b5bf..01d81f08 100644 --- a/components/api-server/openapi/openapi.managedClusters.yaml +++ b/components/api-server/openapi/openapi.managedClusters.yaml @@ -240,6 +240,12 @@ components: type: string api_server_url: type: string + profile_id: + type: string + description: Default GatewayProfile identifier inherited by gateways created on this cluster when they do not supply their own profile_id + database_id: + type: string + description: Default ManagedDatabase identifier for gateways placed on this cluster # NEW SCHEMA START ManagedClusterList: # NEW SCHEMA END @@ -268,6 +274,11 @@ components: type: string api_server_url: type: string + profile_id: + type: string + description: Default GatewayProfile for gateways on this cluster; set to empty string to clear + database_id: + type: string parameters: id: name: id diff --git a/components/api-server/openapi/openapi.yaml b/components/api-server/openapi/openapi.yaml index 28f7c4e1..fbf238d1 100644 --- a/components/api-server/openapi/openapi.yaml +++ b/components/api-server/openapi/openapi.yaml @@ -60,6 +60,10 @@ paths: $ref: 'openapi.roleBindings.yaml#/paths/~1api~1hypershell~1v1~1role_bindings' /api/hypershell/v1/role_bindings/{id}: $ref: 'openapi.roleBindings.yaml#/paths/~1api~1hypershell~1v1~1role_bindings~1{id}' + /api/hypershell/v1/gateway_profiles: + $ref: 'openapi.gatewayProfiles.yaml#/paths/~1api~1hypershell~1v1~1gateway_profiles' + /api/hypershell/v1/gateway_profiles/{id}: + $ref: 'openapi.gatewayProfiles.yaml#/paths/~1api~1hypershell~1v1~1gateway_profiles~1{id}' # AUTO-ADD NEW PATHS components: schemas: @@ -162,6 +166,12 @@ components: $ref: 'openapi.roleBindings.yaml#/components/schemas/RoleBinding' RoleBindingList: $ref: 'openapi.roleBindings.yaml#/components/schemas/RoleBindingList' + GatewayProfile: + $ref: 'openapi.gatewayProfiles.yaml#/components/schemas/GatewayProfile' + GatewayProfileList: + $ref: 'openapi.gatewayProfiles.yaml#/components/schemas/GatewayProfileList' + GatewayProfilePatchRequest: + $ref: 'openapi.gatewayProfiles.yaml#/components/schemas/GatewayProfilePatchRequest' # AUTO-ADD NEW SCHEMAS parameters: id: diff --git a/components/api-server/pkg/api/grpc/hypershell/v1/gateway_profiles.pb.go b/components/api-server/pkg/api/grpc/hypershell/v1/gateway_profiles.pb.go new file mode 100644 index 00000000..96b8328c --- /dev/null +++ b/components/api-server/pkg/api/grpc/hypershell/v1/gateway_profiles.pb.go @@ -0,0 +1,1020 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.11 +// protoc (unknown) +// source: hypershell/v1/gateway_profiles.proto + +package hypershell_v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type GatewayProfile struct { + state protoimpl.MessageState `protogen:"open.v1"` + Metadata *ObjectReference `protobuf:"bytes,1,opt,name=metadata,proto3" json:"metadata,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + Description *string `protobuf:"bytes,3,opt,name=description,proto3,oneof" json:"description,omitempty"` + CpuRequestTotal *string `protobuf:"bytes,4,opt,name=cpu_request_total,json=cpuRequestTotal,proto3,oneof" json:"cpu_request_total,omitempty"` + CpuLimitTotal *string `protobuf:"bytes,5,opt,name=cpu_limit_total,json=cpuLimitTotal,proto3,oneof" json:"cpu_limit_total,omitempty"` + MemoryRequestTotal *string `protobuf:"bytes,6,opt,name=memory_request_total,json=memoryRequestTotal,proto3,oneof" json:"memory_request_total,omitempty"` + MemoryLimitTotal *string `protobuf:"bytes,7,opt,name=memory_limit_total,json=memoryLimitTotal,proto3,oneof" json:"memory_limit_total,omitempty"` + EphemeralStorageTotal *string `protobuf:"bytes,8,opt,name=ephemeral_storage_total,json=ephemeralStorageTotal,proto3,oneof" json:"ephemeral_storage_total,omitempty"` + PodCount *int32 `protobuf:"varint,9,opt,name=pod_count,json=podCount,proto3,oneof" json:"pod_count,omitempty"` + PvcCount *int32 `protobuf:"varint,10,opt,name=pvc_count,json=pvcCount,proto3,oneof" json:"pvc_count,omitempty"` + ContainerCpuRequestDefault *string `protobuf:"bytes,11,opt,name=container_cpu_request_default,json=containerCpuRequestDefault,proto3,oneof" json:"container_cpu_request_default,omitempty"` + ContainerCpuLimitMax *string `protobuf:"bytes,12,opt,name=container_cpu_limit_max,json=containerCpuLimitMax,proto3,oneof" json:"container_cpu_limit_max,omitempty"` + ContainerMemoryRequestDefault *string `protobuf:"bytes,13,opt,name=container_memory_request_default,json=containerMemoryRequestDefault,proto3,oneof" json:"container_memory_request_default,omitempty"` + ContainerMemoryLimitMax *string `protobuf:"bytes,14,opt,name=container_memory_limit_max,json=containerMemoryLimitMax,proto3,oneof" json:"container_memory_limit_max,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GatewayProfile) Reset() { + *x = GatewayProfile{} + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GatewayProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GatewayProfile) ProtoMessage() {} + +func (x *GatewayProfile) ProtoReflect() protoreflect.Message { + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GatewayProfile.ProtoReflect.Descriptor instead. +func (*GatewayProfile) Descriptor() ([]byte, []int) { + return file_hypershell_v1_gateway_profiles_proto_rawDescGZIP(), []int{0} +} + +func (x *GatewayProfile) GetMetadata() *ObjectReference { + if x != nil { + return x.Metadata + } + return nil +} + +func (x *GatewayProfile) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *GatewayProfile) GetDescription() string { + if x != nil && x.Description != nil { + return *x.Description + } + return "" +} + +func (x *GatewayProfile) GetCpuRequestTotal() string { + if x != nil && x.CpuRequestTotal != nil { + return *x.CpuRequestTotal + } + return "" +} + +func (x *GatewayProfile) GetCpuLimitTotal() string { + if x != nil && x.CpuLimitTotal != nil { + return *x.CpuLimitTotal + } + return "" +} + +func (x *GatewayProfile) GetMemoryRequestTotal() string { + if x != nil && x.MemoryRequestTotal != nil { + return *x.MemoryRequestTotal + } + return "" +} + +func (x *GatewayProfile) GetMemoryLimitTotal() string { + if x != nil && x.MemoryLimitTotal != nil { + return *x.MemoryLimitTotal + } + return "" +} + +func (x *GatewayProfile) GetEphemeralStorageTotal() string { + if x != nil && x.EphemeralStorageTotal != nil { + return *x.EphemeralStorageTotal + } + return "" +} + +func (x *GatewayProfile) GetPodCount() int32 { + if x != nil && x.PodCount != nil { + return *x.PodCount + } + return 0 +} + +func (x *GatewayProfile) GetPvcCount() int32 { + if x != nil && x.PvcCount != nil { + return *x.PvcCount + } + return 0 +} + +func (x *GatewayProfile) GetContainerCpuRequestDefault() string { + if x != nil && x.ContainerCpuRequestDefault != nil { + return *x.ContainerCpuRequestDefault + } + return "" +} + +func (x *GatewayProfile) GetContainerCpuLimitMax() string { + if x != nil && x.ContainerCpuLimitMax != nil { + return *x.ContainerCpuLimitMax + } + return "" +} + +func (x *GatewayProfile) GetContainerMemoryRequestDefault() string { + if x != nil && x.ContainerMemoryRequestDefault != nil { + return *x.ContainerMemoryRequestDefault + } + return "" +} + +func (x *GatewayProfile) GetContainerMemoryLimitMax() string { + if x != nil && x.ContainerMemoryLimitMax != nil { + return *x.ContainerMemoryLimitMax + } + return "" +} + +type CreateGatewayProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Description *string `protobuf:"bytes,2,opt,name=description,proto3,oneof" json:"description,omitempty"` + CpuRequestTotal *string `protobuf:"bytes,3,opt,name=cpu_request_total,json=cpuRequestTotal,proto3,oneof" json:"cpu_request_total,omitempty"` + CpuLimitTotal *string `protobuf:"bytes,4,opt,name=cpu_limit_total,json=cpuLimitTotal,proto3,oneof" json:"cpu_limit_total,omitempty"` + MemoryRequestTotal *string `protobuf:"bytes,5,opt,name=memory_request_total,json=memoryRequestTotal,proto3,oneof" json:"memory_request_total,omitempty"` + MemoryLimitTotal *string `protobuf:"bytes,6,opt,name=memory_limit_total,json=memoryLimitTotal,proto3,oneof" json:"memory_limit_total,omitempty"` + EphemeralStorageTotal *string `protobuf:"bytes,7,opt,name=ephemeral_storage_total,json=ephemeralStorageTotal,proto3,oneof" json:"ephemeral_storage_total,omitempty"` + PodCount *int32 `protobuf:"varint,8,opt,name=pod_count,json=podCount,proto3,oneof" json:"pod_count,omitempty"` + PvcCount *int32 `protobuf:"varint,9,opt,name=pvc_count,json=pvcCount,proto3,oneof" json:"pvc_count,omitempty"` + ContainerCpuRequestDefault *string `protobuf:"bytes,10,opt,name=container_cpu_request_default,json=containerCpuRequestDefault,proto3,oneof" json:"container_cpu_request_default,omitempty"` + ContainerCpuLimitMax *string `protobuf:"bytes,11,opt,name=container_cpu_limit_max,json=containerCpuLimitMax,proto3,oneof" json:"container_cpu_limit_max,omitempty"` + ContainerMemoryRequestDefault *string `protobuf:"bytes,12,opt,name=container_memory_request_default,json=containerMemoryRequestDefault,proto3,oneof" json:"container_memory_request_default,omitempty"` + ContainerMemoryLimitMax *string `protobuf:"bytes,13,opt,name=container_memory_limit_max,json=containerMemoryLimitMax,proto3,oneof" json:"container_memory_limit_max,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateGatewayProfileRequest) Reset() { + *x = CreateGatewayProfileRequest{} + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateGatewayProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateGatewayProfileRequest) ProtoMessage() {} + +func (x *CreateGatewayProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[1] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateGatewayProfileRequest.ProtoReflect.Descriptor instead. +func (*CreateGatewayProfileRequest) Descriptor() ([]byte, []int) { + return file_hypershell_v1_gateway_profiles_proto_rawDescGZIP(), []int{1} +} + +func (x *CreateGatewayProfileRequest) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *CreateGatewayProfileRequest) GetDescription() string { + if x != nil && x.Description != nil { + return *x.Description + } + return "" +} + +func (x *CreateGatewayProfileRequest) GetCpuRequestTotal() string { + if x != nil && x.CpuRequestTotal != nil { + return *x.CpuRequestTotal + } + return "" +} + +func (x *CreateGatewayProfileRequest) GetCpuLimitTotal() string { + if x != nil && x.CpuLimitTotal != nil { + return *x.CpuLimitTotal + } + return "" +} + +func (x *CreateGatewayProfileRequest) GetMemoryRequestTotal() string { + if x != nil && x.MemoryRequestTotal != nil { + return *x.MemoryRequestTotal + } + return "" +} + +func (x *CreateGatewayProfileRequest) GetMemoryLimitTotal() string { + if x != nil && x.MemoryLimitTotal != nil { + return *x.MemoryLimitTotal + } + return "" +} + +func (x *CreateGatewayProfileRequest) GetEphemeralStorageTotal() string { + if x != nil && x.EphemeralStorageTotal != nil { + return *x.EphemeralStorageTotal + } + return "" +} + +func (x *CreateGatewayProfileRequest) GetPodCount() int32 { + if x != nil && x.PodCount != nil { + return *x.PodCount + } + return 0 +} + +func (x *CreateGatewayProfileRequest) GetPvcCount() int32 { + if x != nil && x.PvcCount != nil { + return *x.PvcCount + } + return 0 +} + +func (x *CreateGatewayProfileRequest) GetContainerCpuRequestDefault() string { + if x != nil && x.ContainerCpuRequestDefault != nil { + return *x.ContainerCpuRequestDefault + } + return "" +} + +func (x *CreateGatewayProfileRequest) GetContainerCpuLimitMax() string { + if x != nil && x.ContainerCpuLimitMax != nil { + return *x.ContainerCpuLimitMax + } + return "" +} + +func (x *CreateGatewayProfileRequest) GetContainerMemoryRequestDefault() string { + if x != nil && x.ContainerMemoryRequestDefault != nil { + return *x.ContainerMemoryRequestDefault + } + return "" +} + +func (x *CreateGatewayProfileRequest) GetContainerMemoryLimitMax() string { + if x != nil && x.ContainerMemoryLimitMax != nil { + return *x.ContainerMemoryLimitMax + } + return "" +} + +type CreateGatewayProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + GatewayProfile *GatewayProfile `protobuf:"bytes,1,opt,name=gateway_profile,json=gatewayProfile,proto3" json:"gateway_profile,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *CreateGatewayProfileResponse) Reset() { + *x = CreateGatewayProfileResponse{} + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *CreateGatewayProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateGatewayProfileResponse) ProtoMessage() {} + +func (x *CreateGatewayProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[2] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateGatewayProfileResponse.ProtoReflect.Descriptor instead. +func (*CreateGatewayProfileResponse) Descriptor() ([]byte, []int) { + return file_hypershell_v1_gateway_profiles_proto_rawDescGZIP(), []int{2} +} + +func (x *CreateGatewayProfileResponse) GetGatewayProfile() *GatewayProfile { + if x != nil { + return x.GatewayProfile + } + return nil +} + +type GetGatewayProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGatewayProfileRequest) Reset() { + *x = GetGatewayProfileRequest{} + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGatewayProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGatewayProfileRequest) ProtoMessage() {} + +func (x *GetGatewayProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[3] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetGatewayProfileRequest.ProtoReflect.Descriptor instead. +func (*GetGatewayProfileRequest) Descriptor() ([]byte, []int) { + return file_hypershell_v1_gateway_profiles_proto_rawDescGZIP(), []int{3} +} + +func (x *GetGatewayProfileRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type GetGatewayProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + GatewayProfile *GatewayProfile `protobuf:"bytes,1,opt,name=gateway_profile,json=gatewayProfile,proto3" json:"gateway_profile,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetGatewayProfileResponse) Reset() { + *x = GetGatewayProfileResponse{} + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetGatewayProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetGatewayProfileResponse) ProtoMessage() {} + +func (x *GetGatewayProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[4] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetGatewayProfileResponse.ProtoReflect.Descriptor instead. +func (*GetGatewayProfileResponse) Descriptor() ([]byte, []int) { + return file_hypershell_v1_gateway_profiles_proto_rawDescGZIP(), []int{4} +} + +func (x *GetGatewayProfileResponse) GetGatewayProfile() *GatewayProfile { + if x != nil { + return x.GatewayProfile + } + return nil +} + +type UpdateGatewayProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + Name *string `protobuf:"bytes,2,opt,name=name,proto3,oneof" json:"name,omitempty"` + Description *string `protobuf:"bytes,3,opt,name=description,proto3,oneof" json:"description,omitempty"` + CpuRequestTotal *string `protobuf:"bytes,4,opt,name=cpu_request_total,json=cpuRequestTotal,proto3,oneof" json:"cpu_request_total,omitempty"` + CpuLimitTotal *string `protobuf:"bytes,5,opt,name=cpu_limit_total,json=cpuLimitTotal,proto3,oneof" json:"cpu_limit_total,omitempty"` + MemoryRequestTotal *string `protobuf:"bytes,6,opt,name=memory_request_total,json=memoryRequestTotal,proto3,oneof" json:"memory_request_total,omitempty"` + MemoryLimitTotal *string `protobuf:"bytes,7,opt,name=memory_limit_total,json=memoryLimitTotal,proto3,oneof" json:"memory_limit_total,omitempty"` + EphemeralStorageTotal *string `protobuf:"bytes,8,opt,name=ephemeral_storage_total,json=ephemeralStorageTotal,proto3,oneof" json:"ephemeral_storage_total,omitempty"` + PodCount *int32 `protobuf:"varint,9,opt,name=pod_count,json=podCount,proto3,oneof" json:"pod_count,omitempty"` + PvcCount *int32 `protobuf:"varint,10,opt,name=pvc_count,json=pvcCount,proto3,oneof" json:"pvc_count,omitempty"` + ContainerCpuRequestDefault *string `protobuf:"bytes,11,opt,name=container_cpu_request_default,json=containerCpuRequestDefault,proto3,oneof" json:"container_cpu_request_default,omitempty"` + ContainerCpuLimitMax *string `protobuf:"bytes,12,opt,name=container_cpu_limit_max,json=containerCpuLimitMax,proto3,oneof" json:"container_cpu_limit_max,omitempty"` + ContainerMemoryRequestDefault *string `protobuf:"bytes,13,opt,name=container_memory_request_default,json=containerMemoryRequestDefault,proto3,oneof" json:"container_memory_request_default,omitempty"` + ContainerMemoryLimitMax *string `protobuf:"bytes,14,opt,name=container_memory_limit_max,json=containerMemoryLimitMax,proto3,oneof" json:"container_memory_limit_max,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateGatewayProfileRequest) Reset() { + *x = UpdateGatewayProfileRequest{} + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateGatewayProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateGatewayProfileRequest) ProtoMessage() {} + +func (x *UpdateGatewayProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[5] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateGatewayProfileRequest.ProtoReflect.Descriptor instead. +func (*UpdateGatewayProfileRequest) Descriptor() ([]byte, []int) { + return file_hypershell_v1_gateway_profiles_proto_rawDescGZIP(), []int{5} +} + +func (x *UpdateGatewayProfileRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +func (x *UpdateGatewayProfileRequest) GetName() string { + if x != nil && x.Name != nil { + return *x.Name + } + return "" +} + +func (x *UpdateGatewayProfileRequest) GetDescription() string { + if x != nil && x.Description != nil { + return *x.Description + } + return "" +} + +func (x *UpdateGatewayProfileRequest) GetCpuRequestTotal() string { + if x != nil && x.CpuRequestTotal != nil { + return *x.CpuRequestTotal + } + return "" +} + +func (x *UpdateGatewayProfileRequest) GetCpuLimitTotal() string { + if x != nil && x.CpuLimitTotal != nil { + return *x.CpuLimitTotal + } + return "" +} + +func (x *UpdateGatewayProfileRequest) GetMemoryRequestTotal() string { + if x != nil && x.MemoryRequestTotal != nil { + return *x.MemoryRequestTotal + } + return "" +} + +func (x *UpdateGatewayProfileRequest) GetMemoryLimitTotal() string { + if x != nil && x.MemoryLimitTotal != nil { + return *x.MemoryLimitTotal + } + return "" +} + +func (x *UpdateGatewayProfileRequest) GetEphemeralStorageTotal() string { + if x != nil && x.EphemeralStorageTotal != nil { + return *x.EphemeralStorageTotal + } + return "" +} + +func (x *UpdateGatewayProfileRequest) GetPodCount() int32 { + if x != nil && x.PodCount != nil { + return *x.PodCount + } + return 0 +} + +func (x *UpdateGatewayProfileRequest) GetPvcCount() int32 { + if x != nil && x.PvcCount != nil { + return *x.PvcCount + } + return 0 +} + +func (x *UpdateGatewayProfileRequest) GetContainerCpuRequestDefault() string { + if x != nil && x.ContainerCpuRequestDefault != nil { + return *x.ContainerCpuRequestDefault + } + return "" +} + +func (x *UpdateGatewayProfileRequest) GetContainerCpuLimitMax() string { + if x != nil && x.ContainerCpuLimitMax != nil { + return *x.ContainerCpuLimitMax + } + return "" +} + +func (x *UpdateGatewayProfileRequest) GetContainerMemoryRequestDefault() string { + if x != nil && x.ContainerMemoryRequestDefault != nil { + return *x.ContainerMemoryRequestDefault + } + return "" +} + +func (x *UpdateGatewayProfileRequest) GetContainerMemoryLimitMax() string { + if x != nil && x.ContainerMemoryLimitMax != nil { + return *x.ContainerMemoryLimitMax + } + return "" +} + +type UpdateGatewayProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + GatewayProfile *GatewayProfile `protobuf:"bytes,1,opt,name=gateway_profile,json=gatewayProfile,proto3" json:"gateway_profile,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *UpdateGatewayProfileResponse) Reset() { + *x = UpdateGatewayProfileResponse{} + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *UpdateGatewayProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UpdateGatewayProfileResponse) ProtoMessage() {} + +func (x *UpdateGatewayProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[6] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UpdateGatewayProfileResponse.ProtoReflect.Descriptor instead. +func (*UpdateGatewayProfileResponse) Descriptor() ([]byte, []int) { + return file_hypershell_v1_gateway_profiles_proto_rawDescGZIP(), []int{6} +} + +func (x *UpdateGatewayProfileResponse) GetGatewayProfile() *GatewayProfile { + if x != nil { + return x.GatewayProfile + } + return nil +} + +type DeleteGatewayProfileRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteGatewayProfileRequest) Reset() { + *x = DeleteGatewayProfileRequest{} + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteGatewayProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteGatewayProfileRequest) ProtoMessage() {} + +func (x *DeleteGatewayProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteGatewayProfileRequest.ProtoReflect.Descriptor instead. +func (*DeleteGatewayProfileRequest) Descriptor() ([]byte, []int) { + return file_hypershell_v1_gateway_profiles_proto_rawDescGZIP(), []int{7} +} + +func (x *DeleteGatewayProfileRequest) GetId() string { + if x != nil { + return x.Id + } + return "" +} + +type ListGatewayProfilesRequest struct { + state protoimpl.MessageState `protogen:"open.v1"` + Page int32 `protobuf:"varint,1,opt,name=page,proto3" json:"page,omitempty"` + Size int32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListGatewayProfilesRequest) Reset() { + *x = ListGatewayProfilesRequest{} + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListGatewayProfilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListGatewayProfilesRequest) ProtoMessage() {} + +func (x *ListGatewayProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListGatewayProfilesRequest.ProtoReflect.Descriptor instead. +func (*ListGatewayProfilesRequest) Descriptor() ([]byte, []int) { + return file_hypershell_v1_gateway_profiles_proto_rawDescGZIP(), []int{8} +} + +func (x *ListGatewayProfilesRequest) GetPage() int32 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ListGatewayProfilesRequest) GetSize() int32 { + if x != nil { + return x.Size + } + return 0 +} + +type ListGatewayProfilesResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + Items []*GatewayProfile `protobuf:"bytes,1,rep,name=items,proto3" json:"items,omitempty"` + Metadata *ListMeta `protobuf:"bytes,2,opt,name=metadata,proto3" json:"metadata,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *ListGatewayProfilesResponse) Reset() { + *x = ListGatewayProfilesResponse{} + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListGatewayProfilesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListGatewayProfilesResponse) ProtoMessage() {} + +func (x *ListGatewayProfilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListGatewayProfilesResponse.ProtoReflect.Descriptor instead. +func (*ListGatewayProfilesResponse) Descriptor() ([]byte, []int) { + return file_hypershell_v1_gateway_profiles_proto_rawDescGZIP(), []int{9} +} + +func (x *ListGatewayProfilesResponse) GetItems() []*GatewayProfile { + if x != nil { + return x.Items + } + return nil +} + +func (x *ListGatewayProfilesResponse) GetMetadata() *ListMeta { + if x != nil { + return x.Metadata + } + return nil +} + +type DeleteGatewayProfileResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *DeleteGatewayProfileResponse) Reset() { + *x = DeleteGatewayProfileResponse{} + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[10] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *DeleteGatewayProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*DeleteGatewayProfileResponse) ProtoMessage() {} + +func (x *DeleteGatewayProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_hypershell_v1_gateway_profiles_proto_msgTypes[10] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use DeleteGatewayProfileResponse.ProtoReflect.Descriptor instead. +func (*DeleteGatewayProfileResponse) Descriptor() ([]byte, []int) { + return file_hypershell_v1_gateway_profiles_proto_rawDescGZIP(), []int{10} +} + +var File_hypershell_v1_gateway_profiles_proto protoreflect.FileDescriptor + +const file_hypershell_v1_gateway_profiles_proto_rawDesc = "" + + "\n" + + "$hypershell/v1/gateway_profiles.proto\x12\rhypershell.v1\x1a\x1ahypershell/v1/common.proto\"\x88\b\n" + + "\x0eGatewayProfile\x12:\n" + + "\bmetadata\x18\x01 \x01(\v2\x1e.hypershell.v1.ObjectReferenceR\bmetadata\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x12%\n" + + "\vdescription\x18\x03 \x01(\tH\x00R\vdescription\x88\x01\x01\x12/\n" + + "\x11cpu_request_total\x18\x04 \x01(\tH\x01R\x0fcpuRequestTotal\x88\x01\x01\x12+\n" + + "\x0fcpu_limit_total\x18\x05 \x01(\tH\x02R\rcpuLimitTotal\x88\x01\x01\x125\n" + + "\x14memory_request_total\x18\x06 \x01(\tH\x03R\x12memoryRequestTotal\x88\x01\x01\x121\n" + + "\x12memory_limit_total\x18\a \x01(\tH\x04R\x10memoryLimitTotal\x88\x01\x01\x12;\n" + + "\x17ephemeral_storage_total\x18\b \x01(\tH\x05R\x15ephemeralStorageTotal\x88\x01\x01\x12 \n" + + "\tpod_count\x18\t \x01(\x05H\x06R\bpodCount\x88\x01\x01\x12 \n" + + "\tpvc_count\x18\n" + + " \x01(\x05H\aR\bpvcCount\x88\x01\x01\x12F\n" + + "\x1dcontainer_cpu_request_default\x18\v \x01(\tH\bR\x1acontainerCpuRequestDefault\x88\x01\x01\x12:\n" + + "\x17container_cpu_limit_max\x18\f \x01(\tH\tR\x14containerCpuLimitMax\x88\x01\x01\x12L\n" + + " container_memory_request_default\x18\r \x01(\tH\n" + + "R\x1dcontainerMemoryRequestDefault\x88\x01\x01\x12@\n" + + "\x1acontainer_memory_limit_max\x18\x0e \x01(\tH\vR\x17containerMemoryLimitMax\x88\x01\x01B\x0e\n" + + "\f_descriptionB\x14\n" + + "\x12_cpu_request_totalB\x12\n" + + "\x10_cpu_limit_totalB\x17\n" + + "\x15_memory_request_totalB\x15\n" + + "\x13_memory_limit_totalB\x1a\n" + + "\x18_ephemeral_storage_totalB\f\n" + + "\n" + + "_pod_countB\f\n" + + "\n" + + "_pvc_countB \n" + + "\x1e_container_cpu_request_defaultB\x1a\n" + + "\x18_container_cpu_limit_maxB#\n" + + "!_container_memory_request_defaultB\x1d\n" + + "\x1b_container_memory_limit_max\"\xd9\a\n" + + "\x1bCreateGatewayProfileRequest\x12\x12\n" + + "\x04name\x18\x01 \x01(\tR\x04name\x12%\n" + + "\vdescription\x18\x02 \x01(\tH\x00R\vdescription\x88\x01\x01\x12/\n" + + "\x11cpu_request_total\x18\x03 \x01(\tH\x01R\x0fcpuRequestTotal\x88\x01\x01\x12+\n" + + "\x0fcpu_limit_total\x18\x04 \x01(\tH\x02R\rcpuLimitTotal\x88\x01\x01\x125\n" + + "\x14memory_request_total\x18\x05 \x01(\tH\x03R\x12memoryRequestTotal\x88\x01\x01\x121\n" + + "\x12memory_limit_total\x18\x06 \x01(\tH\x04R\x10memoryLimitTotal\x88\x01\x01\x12;\n" + + "\x17ephemeral_storage_total\x18\a \x01(\tH\x05R\x15ephemeralStorageTotal\x88\x01\x01\x12 \n" + + "\tpod_count\x18\b \x01(\x05H\x06R\bpodCount\x88\x01\x01\x12 \n" + + "\tpvc_count\x18\t \x01(\x05H\aR\bpvcCount\x88\x01\x01\x12F\n" + + "\x1dcontainer_cpu_request_default\x18\n" + + " \x01(\tH\bR\x1acontainerCpuRequestDefault\x88\x01\x01\x12:\n" + + "\x17container_cpu_limit_max\x18\v \x01(\tH\tR\x14containerCpuLimitMax\x88\x01\x01\x12L\n" + + " container_memory_request_default\x18\f \x01(\tH\n" + + "R\x1dcontainerMemoryRequestDefault\x88\x01\x01\x12@\n" + + "\x1acontainer_memory_limit_max\x18\r \x01(\tH\vR\x17containerMemoryLimitMax\x88\x01\x01B\x0e\n" + + "\f_descriptionB\x14\n" + + "\x12_cpu_request_totalB\x12\n" + + "\x10_cpu_limit_totalB\x17\n" + + "\x15_memory_request_totalB\x15\n" + + "\x13_memory_limit_totalB\x1a\n" + + "\x18_ephemeral_storage_totalB\f\n" + + "\n" + + "_pod_countB\f\n" + + "\n" + + "_pvc_countB \n" + + "\x1e_container_cpu_request_defaultB\x1a\n" + + "\x18_container_cpu_limit_maxB#\n" + + "!_container_memory_request_defaultB\x1d\n" + + "\x1b_container_memory_limit_max\"f\n" + + "\x1cCreateGatewayProfileResponse\x12F\n" + + "\x0fgateway_profile\x18\x01 \x01(\v2\x1d.hypershell.v1.GatewayProfileR\x0egatewayProfile\"*\n" + + "\x18GetGatewayProfileRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"c\n" + + "\x19GetGatewayProfileResponse\x12F\n" + + "\x0fgateway_profile\x18\x01 \x01(\v2\x1d.hypershell.v1.GatewayProfileR\x0egatewayProfile\"\xf7\a\n" + + "\x1bUpdateGatewayProfileRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" + + "\x04name\x18\x02 \x01(\tH\x00R\x04name\x88\x01\x01\x12%\n" + + "\vdescription\x18\x03 \x01(\tH\x01R\vdescription\x88\x01\x01\x12/\n" + + "\x11cpu_request_total\x18\x04 \x01(\tH\x02R\x0fcpuRequestTotal\x88\x01\x01\x12+\n" + + "\x0fcpu_limit_total\x18\x05 \x01(\tH\x03R\rcpuLimitTotal\x88\x01\x01\x125\n" + + "\x14memory_request_total\x18\x06 \x01(\tH\x04R\x12memoryRequestTotal\x88\x01\x01\x121\n" + + "\x12memory_limit_total\x18\a \x01(\tH\x05R\x10memoryLimitTotal\x88\x01\x01\x12;\n" + + "\x17ephemeral_storage_total\x18\b \x01(\tH\x06R\x15ephemeralStorageTotal\x88\x01\x01\x12 \n" + + "\tpod_count\x18\t \x01(\x05H\aR\bpodCount\x88\x01\x01\x12 \n" + + "\tpvc_count\x18\n" + + " \x01(\x05H\bR\bpvcCount\x88\x01\x01\x12F\n" + + "\x1dcontainer_cpu_request_default\x18\v \x01(\tH\tR\x1acontainerCpuRequestDefault\x88\x01\x01\x12:\n" + + "\x17container_cpu_limit_max\x18\f \x01(\tH\n" + + "R\x14containerCpuLimitMax\x88\x01\x01\x12L\n" + + " container_memory_request_default\x18\r \x01(\tH\vR\x1dcontainerMemoryRequestDefault\x88\x01\x01\x12@\n" + + "\x1acontainer_memory_limit_max\x18\x0e \x01(\tH\fR\x17containerMemoryLimitMax\x88\x01\x01B\a\n" + + "\x05_nameB\x0e\n" + + "\f_descriptionB\x14\n" + + "\x12_cpu_request_totalB\x12\n" + + "\x10_cpu_limit_totalB\x17\n" + + "\x15_memory_request_totalB\x15\n" + + "\x13_memory_limit_totalB\x1a\n" + + "\x18_ephemeral_storage_totalB\f\n" + + "\n" + + "_pod_countB\f\n" + + "\n" + + "_pvc_countB \n" + + "\x1e_container_cpu_request_defaultB\x1a\n" + + "\x18_container_cpu_limit_maxB#\n" + + "!_container_memory_request_defaultB\x1d\n" + + "\x1b_container_memory_limit_max\"f\n" + + "\x1cUpdateGatewayProfileResponse\x12F\n" + + "\x0fgateway_profile\x18\x01 \x01(\v2\x1d.hypershell.v1.GatewayProfileR\x0egatewayProfile\"-\n" + + "\x1bDeleteGatewayProfileRequest\x12\x0e\n" + + "\x02id\x18\x01 \x01(\tR\x02id\"D\n" + + "\x1aListGatewayProfilesRequest\x12\x12\n" + + "\x04page\x18\x01 \x01(\x05R\x04page\x12\x12\n" + + "\x04size\x18\x02 \x01(\x05R\x04size\"\x87\x01\n" + + "\x1bListGatewayProfilesResponse\x123\n" + + "\x05items\x18\x01 \x03(\v2\x1d.hypershell.v1.GatewayProfileR\x05items\x123\n" + + "\bmetadata\x18\x02 \x01(\v2\x17.hypershell.v1.ListMetaR\bmetadata\"\x1e\n" + + "\x1cDeleteGatewayProfileResponse2\xc0\x04\n" + + "\x15GatewayProfileService\x12f\n" + + "\x11GetGatewayProfile\x12'.hypershell.v1.GetGatewayProfileRequest\x1a(.hypershell.v1.GetGatewayProfileResponse\x12o\n" + + "\x14CreateGatewayProfile\x12*.hypershell.v1.CreateGatewayProfileRequest\x1a+.hypershell.v1.CreateGatewayProfileResponse\x12o\n" + + "\x14UpdateGatewayProfile\x12*.hypershell.v1.UpdateGatewayProfileRequest\x1a+.hypershell.v1.UpdateGatewayProfileResponse\x12o\n" + + "\x14DeleteGatewayProfile\x12*.hypershell.v1.DeleteGatewayProfileRequest\x1a+.hypershell.v1.DeleteGatewayProfileResponse\x12l\n" + + "\x13ListGatewayProfiles\x12).hypershell.v1.ListGatewayProfilesRequest\x1a*.hypershell.v1.ListGatewayProfilesResponseBgZegithub.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1;hypershell_v1b\x06proto3" + +var ( + file_hypershell_v1_gateway_profiles_proto_rawDescOnce sync.Once + file_hypershell_v1_gateway_profiles_proto_rawDescData []byte +) + +func file_hypershell_v1_gateway_profiles_proto_rawDescGZIP() []byte { + file_hypershell_v1_gateway_profiles_proto_rawDescOnce.Do(func() { + file_hypershell_v1_gateway_profiles_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_hypershell_v1_gateway_profiles_proto_rawDesc), len(file_hypershell_v1_gateway_profiles_proto_rawDesc))) + }) + return file_hypershell_v1_gateway_profiles_proto_rawDescData +} + +var file_hypershell_v1_gateway_profiles_proto_msgTypes = make([]protoimpl.MessageInfo, 11) +var file_hypershell_v1_gateway_profiles_proto_goTypes = []any{ + (*GatewayProfile)(nil), // 0: hypershell.v1.GatewayProfile + (*CreateGatewayProfileRequest)(nil), // 1: hypershell.v1.CreateGatewayProfileRequest + (*CreateGatewayProfileResponse)(nil), // 2: hypershell.v1.CreateGatewayProfileResponse + (*GetGatewayProfileRequest)(nil), // 3: hypershell.v1.GetGatewayProfileRequest + (*GetGatewayProfileResponse)(nil), // 4: hypershell.v1.GetGatewayProfileResponse + (*UpdateGatewayProfileRequest)(nil), // 5: hypershell.v1.UpdateGatewayProfileRequest + (*UpdateGatewayProfileResponse)(nil), // 6: hypershell.v1.UpdateGatewayProfileResponse + (*DeleteGatewayProfileRequest)(nil), // 7: hypershell.v1.DeleteGatewayProfileRequest + (*ListGatewayProfilesRequest)(nil), // 8: hypershell.v1.ListGatewayProfilesRequest + (*ListGatewayProfilesResponse)(nil), // 9: hypershell.v1.ListGatewayProfilesResponse + (*DeleteGatewayProfileResponse)(nil), // 10: hypershell.v1.DeleteGatewayProfileResponse + (*ObjectReference)(nil), // 11: hypershell.v1.ObjectReference + (*ListMeta)(nil), // 12: hypershell.v1.ListMeta +} +var file_hypershell_v1_gateway_profiles_proto_depIdxs = []int32{ + 11, // 0: hypershell.v1.GatewayProfile.metadata:type_name -> hypershell.v1.ObjectReference + 0, // 1: hypershell.v1.CreateGatewayProfileResponse.gateway_profile:type_name -> hypershell.v1.GatewayProfile + 0, // 2: hypershell.v1.GetGatewayProfileResponse.gateway_profile:type_name -> hypershell.v1.GatewayProfile + 0, // 3: hypershell.v1.UpdateGatewayProfileResponse.gateway_profile:type_name -> hypershell.v1.GatewayProfile + 0, // 4: hypershell.v1.ListGatewayProfilesResponse.items:type_name -> hypershell.v1.GatewayProfile + 12, // 5: hypershell.v1.ListGatewayProfilesResponse.metadata:type_name -> hypershell.v1.ListMeta + 3, // 6: hypershell.v1.GatewayProfileService.GetGatewayProfile:input_type -> hypershell.v1.GetGatewayProfileRequest + 1, // 7: hypershell.v1.GatewayProfileService.CreateGatewayProfile:input_type -> hypershell.v1.CreateGatewayProfileRequest + 5, // 8: hypershell.v1.GatewayProfileService.UpdateGatewayProfile:input_type -> hypershell.v1.UpdateGatewayProfileRequest + 7, // 9: hypershell.v1.GatewayProfileService.DeleteGatewayProfile:input_type -> hypershell.v1.DeleteGatewayProfileRequest + 8, // 10: hypershell.v1.GatewayProfileService.ListGatewayProfiles:input_type -> hypershell.v1.ListGatewayProfilesRequest + 4, // 11: hypershell.v1.GatewayProfileService.GetGatewayProfile:output_type -> hypershell.v1.GetGatewayProfileResponse + 2, // 12: hypershell.v1.GatewayProfileService.CreateGatewayProfile:output_type -> hypershell.v1.CreateGatewayProfileResponse + 6, // 13: hypershell.v1.GatewayProfileService.UpdateGatewayProfile:output_type -> hypershell.v1.UpdateGatewayProfileResponse + 10, // 14: hypershell.v1.GatewayProfileService.DeleteGatewayProfile:output_type -> hypershell.v1.DeleteGatewayProfileResponse + 9, // 15: hypershell.v1.GatewayProfileService.ListGatewayProfiles:output_type -> hypershell.v1.ListGatewayProfilesResponse + 11, // [11:16] is the sub-list for method output_type + 6, // [6:11] is the sub-list for method input_type + 6, // [6:6] is the sub-list for extension type_name + 6, // [6:6] is the sub-list for extension extendee + 0, // [0:6] is the sub-list for field type_name +} + +func init() { file_hypershell_v1_gateway_profiles_proto_init() } +func file_hypershell_v1_gateway_profiles_proto_init() { + if File_hypershell_v1_gateway_profiles_proto != nil { + return + } + file_hypershell_v1_common_proto_init() + file_hypershell_v1_gateway_profiles_proto_msgTypes[0].OneofWrappers = []any{} + file_hypershell_v1_gateway_profiles_proto_msgTypes[1].OneofWrappers = []any{} + file_hypershell_v1_gateway_profiles_proto_msgTypes[5].OneofWrappers = []any{} + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_hypershell_v1_gateway_profiles_proto_rawDesc), len(file_hypershell_v1_gateway_profiles_proto_rawDesc)), + NumEnums: 0, + NumMessages: 11, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_hypershell_v1_gateway_profiles_proto_goTypes, + DependencyIndexes: file_hypershell_v1_gateway_profiles_proto_depIdxs, + MessageInfos: file_hypershell_v1_gateway_profiles_proto_msgTypes, + }.Build() + File_hypershell_v1_gateway_profiles_proto = out.File + file_hypershell_v1_gateway_profiles_proto_goTypes = nil + file_hypershell_v1_gateway_profiles_proto_depIdxs = nil +} diff --git a/components/api-server/pkg/api/grpc/hypershell/v1/gateway_profiles_grpc.pb.go b/components/api-server/pkg/api/grpc/hypershell/v1/gateway_profiles_grpc.pb.go new file mode 100644 index 00000000..fbe85424 --- /dev/null +++ b/components/api-server/pkg/api/grpc/hypershell/v1/gateway_profiles_grpc.pb.go @@ -0,0 +1,285 @@ +// Code generated by protoc-gen-go-grpc. DO NOT EDIT. +// versions: +// - protoc-gen-go-grpc v1.6.2 +// - protoc (unknown) +// source: hypershell/v1/gateway_profiles.proto + +package hypershell_v1 + +import ( + context "context" + grpc "google.golang.org/grpc" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" +) + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +// Requires gRPC-Go v1.64.0 or later. +const _ = grpc.SupportPackageIsVersion9 + +const ( + GatewayProfileService_GetGatewayProfile_FullMethodName = "/hypershell.v1.GatewayProfileService/GetGatewayProfile" + GatewayProfileService_CreateGatewayProfile_FullMethodName = "/hypershell.v1.GatewayProfileService/CreateGatewayProfile" + GatewayProfileService_UpdateGatewayProfile_FullMethodName = "/hypershell.v1.GatewayProfileService/UpdateGatewayProfile" + GatewayProfileService_DeleteGatewayProfile_FullMethodName = "/hypershell.v1.GatewayProfileService/DeleteGatewayProfile" + GatewayProfileService_ListGatewayProfiles_FullMethodName = "/hypershell.v1.GatewayProfileService/ListGatewayProfiles" +) + +// GatewayProfileServiceClient is the client API for GatewayProfileService service. +// +// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. +// +// GatewayProfileService intentionally exposes no Watch RPC: the control plane +// does not reconcile GatewayProfile changes. A gateway's quota changes only when +// its profile_id is reassigned (a gateway update, delivered through the existing +// gateway watch stream). The control plane fetches profiles on demand via the +// unary GetGatewayProfile RPC while reconciling a gateway. +type GatewayProfileServiceClient interface { + GetGatewayProfile(ctx context.Context, in *GetGatewayProfileRequest, opts ...grpc.CallOption) (*GetGatewayProfileResponse, error) + CreateGatewayProfile(ctx context.Context, in *CreateGatewayProfileRequest, opts ...grpc.CallOption) (*CreateGatewayProfileResponse, error) + UpdateGatewayProfile(ctx context.Context, in *UpdateGatewayProfileRequest, opts ...grpc.CallOption) (*UpdateGatewayProfileResponse, error) + DeleteGatewayProfile(ctx context.Context, in *DeleteGatewayProfileRequest, opts ...grpc.CallOption) (*DeleteGatewayProfileResponse, error) + ListGatewayProfiles(ctx context.Context, in *ListGatewayProfilesRequest, opts ...grpc.CallOption) (*ListGatewayProfilesResponse, error) +} + +type gatewayProfileServiceClient struct { + cc grpc.ClientConnInterface +} + +func NewGatewayProfileServiceClient(cc grpc.ClientConnInterface) GatewayProfileServiceClient { + return &gatewayProfileServiceClient{cc} +} + +func (c *gatewayProfileServiceClient) GetGatewayProfile(ctx context.Context, in *GetGatewayProfileRequest, opts ...grpc.CallOption) (*GetGatewayProfileResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetGatewayProfileResponse) + err := c.cc.Invoke(ctx, GatewayProfileService_GetGatewayProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gatewayProfileServiceClient) CreateGatewayProfile(ctx context.Context, in *CreateGatewayProfileRequest, opts ...grpc.CallOption) (*CreateGatewayProfileResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(CreateGatewayProfileResponse) + err := c.cc.Invoke(ctx, GatewayProfileService_CreateGatewayProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gatewayProfileServiceClient) UpdateGatewayProfile(ctx context.Context, in *UpdateGatewayProfileRequest, opts ...grpc.CallOption) (*UpdateGatewayProfileResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(UpdateGatewayProfileResponse) + err := c.cc.Invoke(ctx, GatewayProfileService_UpdateGatewayProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gatewayProfileServiceClient) DeleteGatewayProfile(ctx context.Context, in *DeleteGatewayProfileRequest, opts ...grpc.CallOption) (*DeleteGatewayProfileResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(DeleteGatewayProfileResponse) + err := c.cc.Invoke(ctx, GatewayProfileService_DeleteGatewayProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *gatewayProfileServiceClient) ListGatewayProfiles(ctx context.Context, in *ListGatewayProfilesRequest, opts ...grpc.CallOption) (*ListGatewayProfilesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListGatewayProfilesResponse) + err := c.cc.Invoke(ctx, GatewayProfileService_ListGatewayProfiles_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +// GatewayProfileServiceServer is the server API for GatewayProfileService service. +// All implementations must embed UnimplementedGatewayProfileServiceServer +// for forward compatibility. +// +// GatewayProfileService intentionally exposes no Watch RPC: the control plane +// does not reconcile GatewayProfile changes. A gateway's quota changes only when +// its profile_id is reassigned (a gateway update, delivered through the existing +// gateway watch stream). The control plane fetches profiles on demand via the +// unary GetGatewayProfile RPC while reconciling a gateway. +type GatewayProfileServiceServer interface { + GetGatewayProfile(context.Context, *GetGatewayProfileRequest) (*GetGatewayProfileResponse, error) + CreateGatewayProfile(context.Context, *CreateGatewayProfileRequest) (*CreateGatewayProfileResponse, error) + UpdateGatewayProfile(context.Context, *UpdateGatewayProfileRequest) (*UpdateGatewayProfileResponse, error) + DeleteGatewayProfile(context.Context, *DeleteGatewayProfileRequest) (*DeleteGatewayProfileResponse, error) + ListGatewayProfiles(context.Context, *ListGatewayProfilesRequest) (*ListGatewayProfilesResponse, error) + mustEmbedUnimplementedGatewayProfileServiceServer() +} + +// UnimplementedGatewayProfileServiceServer must be embedded to have +// forward compatible implementations. +// +// NOTE: this should be embedded by value instead of pointer to avoid a nil +// pointer dereference when methods are called. +type UnimplementedGatewayProfileServiceServer struct{} + +func (UnimplementedGatewayProfileServiceServer) GetGatewayProfile(context.Context, *GetGatewayProfileRequest) (*GetGatewayProfileResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetGatewayProfile not implemented") +} +func (UnimplementedGatewayProfileServiceServer) CreateGatewayProfile(context.Context, *CreateGatewayProfileRequest) (*CreateGatewayProfileResponse, error) { + return nil, status.Error(codes.Unimplemented, "method CreateGatewayProfile not implemented") +} +func (UnimplementedGatewayProfileServiceServer) UpdateGatewayProfile(context.Context, *UpdateGatewayProfileRequest) (*UpdateGatewayProfileResponse, error) { + return nil, status.Error(codes.Unimplemented, "method UpdateGatewayProfile not implemented") +} +func (UnimplementedGatewayProfileServiceServer) DeleteGatewayProfile(context.Context, *DeleteGatewayProfileRequest) (*DeleteGatewayProfileResponse, error) { + return nil, status.Error(codes.Unimplemented, "method DeleteGatewayProfile not implemented") +} +func (UnimplementedGatewayProfileServiceServer) ListGatewayProfiles(context.Context, *ListGatewayProfilesRequest) (*ListGatewayProfilesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListGatewayProfiles not implemented") +} +func (UnimplementedGatewayProfileServiceServer) mustEmbedUnimplementedGatewayProfileServiceServer() {} +func (UnimplementedGatewayProfileServiceServer) testEmbeddedByValue() {} + +// UnsafeGatewayProfileServiceServer may be embedded to opt out of forward compatibility for this service. +// Use of this interface is not recommended, as added methods to GatewayProfileServiceServer will +// result in compilation errors. +type UnsafeGatewayProfileServiceServer interface { + mustEmbedUnimplementedGatewayProfileServiceServer() +} + +func RegisterGatewayProfileServiceServer(s grpc.ServiceRegistrar, srv GatewayProfileServiceServer) { + // If the following call panics, it indicates UnimplementedGatewayProfileServiceServer was + // embedded by pointer and is nil. This will cause panics if an + // unimplemented method is ever invoked, so we test this at initialization + // time to prevent it from happening at runtime later due to I/O. + if t, ok := srv.(interface{ testEmbeddedByValue() }); ok { + t.testEmbeddedByValue() + } + s.RegisterService(&GatewayProfileService_ServiceDesc, srv) +} + +func _GatewayProfileService_GetGatewayProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetGatewayProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayProfileServiceServer).GetGatewayProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GatewayProfileService_GetGatewayProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayProfileServiceServer).GetGatewayProfile(ctx, req.(*GetGatewayProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GatewayProfileService_CreateGatewayProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateGatewayProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayProfileServiceServer).CreateGatewayProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GatewayProfileService_CreateGatewayProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayProfileServiceServer).CreateGatewayProfile(ctx, req.(*CreateGatewayProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GatewayProfileService_UpdateGatewayProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(UpdateGatewayProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayProfileServiceServer).UpdateGatewayProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GatewayProfileService_UpdateGatewayProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayProfileServiceServer).UpdateGatewayProfile(ctx, req.(*UpdateGatewayProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GatewayProfileService_DeleteGatewayProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(DeleteGatewayProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayProfileServiceServer).DeleteGatewayProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GatewayProfileService_DeleteGatewayProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayProfileServiceServer).DeleteGatewayProfile(ctx, req.(*DeleteGatewayProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _GatewayProfileService_ListGatewayProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListGatewayProfilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(GatewayProfileServiceServer).ListGatewayProfiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: GatewayProfileService_ListGatewayProfiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(GatewayProfileServiceServer).ListGatewayProfiles(ctx, req.(*ListGatewayProfilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +// GatewayProfileService_ServiceDesc is the grpc.ServiceDesc for GatewayProfileService service. +// It's only intended for direct use with grpc.RegisterService, +// and not to be introspected or modified (even as a copy) +var GatewayProfileService_ServiceDesc = grpc.ServiceDesc{ + ServiceName: "hypershell.v1.GatewayProfileService", + HandlerType: (*GatewayProfileServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "GetGatewayProfile", + Handler: _GatewayProfileService_GetGatewayProfile_Handler, + }, + { + MethodName: "CreateGatewayProfile", + Handler: _GatewayProfileService_CreateGatewayProfile_Handler, + }, + { + MethodName: "UpdateGatewayProfile", + Handler: _GatewayProfileService_UpdateGatewayProfile_Handler, + }, + { + MethodName: "DeleteGatewayProfile", + Handler: _GatewayProfileService_DeleteGatewayProfile_Handler, + }, + { + MethodName: "ListGatewayProfiles", + Handler: _GatewayProfileService_ListGatewayProfiles_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "hypershell/v1/gateway_profiles.proto", +} diff --git a/components/api-server/pkg/api/grpc/hypershell/v1/gateways.pb.go b/components/api-server/pkg/api/grpc/hypershell/v1/gateways.pb.go index ee13d468..8675c93d 100644 --- a/components/api-server/pkg/api/grpc/hypershell/v1/gateways.pb.go +++ b/components/api-server/pkg/api/grpc/hypershell/v1/gateways.pb.go @@ -43,6 +43,7 @@ type Gateway struct { CredentialDriver *string `protobuf:"bytes,20,opt,name=credential_driver,json=credentialDriver,proto3,oneof" json:"credential_driver,omitempty"` ActiveSandboxCount *int32 `protobuf:"varint,21,opt,name=active_sandbox_count,json=activeSandboxCount,proto3,oneof" json:"active_sandbox_count,omitempty"` ConsoleAddress *string `protobuf:"bytes,22,opt,name=console_address,json=consoleAddress,proto3,oneof" json:"console_address,omitempty"` + ProfileId *string `protobuf:"bytes,23,opt,name=profile_id,json=profileId,proto3,oneof" json:"profile_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -217,6 +218,13 @@ func (x *Gateway) GetConsoleAddress() string { return "" } +func (x *Gateway) GetProfileId() string { + if x != nil && x.ProfileId != nil { + return *x.ProfileId + } + return "" +} + type CreateGatewayRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -234,6 +242,7 @@ type CreateGatewayRequest struct { Oidc *string `protobuf:"bytes,14,opt,name=oidc,proto3,oneof" json:"oidc,omitempty"` Route *string `protobuf:"bytes,15,opt,name=route,proto3,oneof" json:"route,omitempty"` CredentialDriver *string `protobuf:"bytes,17,opt,name=credential_driver,json=credentialDriver,proto3,oneof" json:"credential_driver,omitempty"` + ProfileId *string `protobuf:"bytes,18,opt,name=profile_id,json=profileId,proto3,oneof" json:"profile_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -373,6 +382,13 @@ func (x *CreateGatewayRequest) GetCredentialDriver() string { return "" } +func (x *CreateGatewayRequest) GetProfileId() string { + if x != nil && x.ProfileId != nil { + return *x.ProfileId + } + return "" +} + type CreateGatewayResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Gateway *Gateway `protobuf:"bytes,1,opt,name=gateway,proto3" json:"gateway,omitempty"` @@ -529,6 +545,7 @@ type UpdateGatewayRequest struct { // so the whole-row UpdateGateway path can never clobber it. See // openshell-gateway-sandbox-count.spec.md. ConsoleAddress *string `protobuf:"bytes,20,opt,name=console_address,json=consoleAddress,proto3,oneof" json:"console_address,omitempty"` + ProfileId *string `protobuf:"bytes,21,opt,name=profile_id,json=profileId,proto3,oneof" json:"profile_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -689,6 +706,13 @@ func (x *UpdateGatewayRequest) GetConsoleAddress() string { return "" } +func (x *UpdateGatewayRequest) GetProfileId() string { + if x != nil && x.ProfileId != nil { + return *x.ProfileId + } + return "" +} + type UpdateGatewayResponse struct { state protoimpl.MessageState `protogen:"open.v1"` Gateway *Gateway `protobuf:"bytes,1,opt,name=gateway,proto3" json:"gateway,omitempty"` @@ -1217,7 +1241,7 @@ var File_hypershell_v1_gateways_proto protoreflect.FileDescriptor const file_hypershell_v1_gateways_proto_rawDesc = "" + "\n" + - "\x1chypershell/v1/gateways.proto\x12\rhypershell.v1\x1a\x1ahypershell/v1/common.proto\"\xc3\a\n" + + "\x1chypershell/v1/gateways.proto\x12\rhypershell.v1\x1a\x1ahypershell/v1/common.proto\"\xf6\a\n" + "\aGateway\x12:\n" + "\bmetadata\x18\x01 \x01(\v2\x1e.hypershell.v1.ObjectReferenceR\bmetadata\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1d\n" + @@ -1243,7 +1267,9 @@ const file_hypershell_v1_gateways_proto_rawDesc = "" + "\x11credential_driver\x18\x14 \x01(\tH\n" + "R\x10credentialDriver\x88\x01\x01\x125\n" + "\x14active_sandbox_count\x18\x15 \x01(\x05H\vR\x12activeSandboxCount\x88\x01\x01\x12,\n" + - "\x0fconsole_address\x18\x16 \x01(\tH\fR\x0econsoleAddress\x88\x01\x01B\x0f\n" + + "\x0fconsole_address\x18\x16 \x01(\tH\fR\x0econsoleAddress\x88\x01\x01\x12\"\n" + + "\n" + + "profile_id\x18\x17 \x01(\tH\rR\tprofileId\x88\x01\x01B\x0f\n" + "\r_external_dnsB\v\n" + "\t_tls_modeB\x0f\n" + "\r_service_typeB\t\n" + @@ -1256,7 +1282,8 @@ const file_hypershell_v1_gateways_proto_rawDesc = "" + "\x06_routeB\x14\n" + "\x12_credential_driverB\x17\n" + "\x15_active_sandbox_countB\x12\n" + - "\x10_console_addressJ\x04\b\x03\x10\x04R\bfleet_id\"\xa8\x05\n" + + "\x10_console_addressB\r\n" + + "\v_profile_idJ\x04\b\x03\x10\x04R\bfleet_id\"\xdb\x05\n" + "\x14CreateGatewayRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1d\n" + "\n" + @@ -1276,7 +1303,10 @@ const file_hypershell_v1_gateways_proto_rawDesc = "" + "\x10server_dns_names\x18\r \x03(\tR\x0eserverDnsNames\x12\x17\n" + "\x04oidc\x18\x0e \x01(\tH\aR\x04oidc\x88\x01\x01\x12\x19\n" + "\x05route\x18\x0f \x01(\tH\bR\x05route\x88\x01\x01\x120\n" + - "\x11credential_driver\x18\x11 \x01(\tH\tR\x10credentialDriver\x88\x01\x01B\x0f\n" + + "\x11credential_driver\x18\x11 \x01(\tH\tR\x10credentialDriver\x88\x01\x01\x12\"\n" + + "\n" + + "profile_id\x18\x12 \x01(\tH\n" + + "R\tprofileId\x88\x01\x01B\x0f\n" + "\r_external_dnsB\v\n" + "\t_tls_modeB\x0f\n" + "\r_service_typeB\t\n" + @@ -1286,13 +1316,14 @@ const file_hypershell_v1_gateways_proto_rawDesc = "" + "\x11_supervisor_imageB\a\n" + "\x05_oidcB\b\n" + "\x06_routeB\x14\n" + - "\x12_credential_driverJ\x04\b\x02\x10\x03R\bfleet_id\"I\n" + + "\x12_credential_driverB\r\n" + + "\v_profile_idJ\x04\b\x02\x10\x03R\bfleet_id\"I\n" + "\x15CreateGatewayResponse\x120\n" + "\agateway\x18\x01 \x01(\v2\x16.hypershell.v1.GatewayR\agateway\"#\n" + "\x11GetGatewayRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\"F\n" + "\x12GetGatewayResponse\x120\n" + - "\agateway\x18\x01 \x01(\v2\x16.hypershell.v1.GatewayR\agateway\"\x81\a\n" + + "\agateway\x18\x01 \x01(\v2\x16.hypershell.v1.GatewayR\agateway\"\xb4\a\n" + "\x14UpdateGatewayRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" + "\x04name\x18\x02 \x01(\tH\x00R\x04name\x88\x01\x01\x12\"\n" + @@ -1316,7 +1347,9 @@ const file_hypershell_v1_gateways_proto_rawDesc = "" + "\x04oidc\x18\x10 \x01(\tH\fR\x04oidc\x88\x01\x01\x12\x19\n" + "\x05route\x18\x11 \x01(\tH\rR\x05route\x88\x01\x01\x120\n" + "\x11credential_driver\x18\x13 \x01(\tH\x0eR\x10credentialDriver\x88\x01\x01\x12,\n" + - "\x0fconsole_address\x18\x14 \x01(\tH\x0fR\x0econsoleAddress\x88\x01\x01B\a\n" + + "\x0fconsole_address\x18\x14 \x01(\tH\x0fR\x0econsoleAddress\x88\x01\x01\x12\"\n" + + "\n" + + "profile_id\x18\x15 \x01(\tH\x10R\tprofileId\x88\x01\x01B\a\n" + "\x05_nameB\r\n" + "\v_cluster_idB\r\n" + "\v_release_idB\x0e\n" + @@ -1332,7 +1365,8 @@ const file_hypershell_v1_gateways_proto_rawDesc = "" + "\x05_oidcB\b\n" + "\x06_routeB\x14\n" + "\x12_credential_driverB\x12\n" + - "\x10_console_addressJ\x04\b\x03\x10\x04R\bfleet_id\"I\n" + + "\x10_console_addressB\r\n" + + "\v_profile_idJ\x04\b\x03\x10\x04R\bfleet_id\"I\n" + "\x15UpdateGatewayResponse\x120\n" + "\agateway\x18\x01 \x01(\v2\x16.hypershell.v1.GatewayR\agateway\"U\n" + "\x1fAdjustActiveSandboxCountRequest\x12\x1c\n" + diff --git a/components/api-server/pkg/api/grpc/hypershell/v1/managed_clusters.pb.go b/components/api-server/pkg/api/grpc/hypershell/v1/managed_clusters.pb.go index 87f2edd7..cd789f0d 100644 --- a/components/api-server/pkg/api/grpc/hypershell/v1/managed_clusters.pb.go +++ b/components/api-server/pkg/api/grpc/hypershell/v1/managed_clusters.pb.go @@ -30,6 +30,8 @@ type ManagedCluster struct { KubeconfigSecret string `protobuf:"bytes,6,opt,name=kubeconfig_secret,json=kubeconfigSecret,proto3" json:"kubeconfig_secret,omitempty"` Status *string `protobuf:"bytes,7,opt,name=status,proto3,oneof" json:"status,omitempty"` ApiServerUrl *string `protobuf:"bytes,8,opt,name=api_server_url,json=apiServerUrl,proto3,oneof" json:"api_server_url,omitempty"` + ProfileId *string `protobuf:"bytes,9,opt,name=profile_id,json=profileId,proto3,oneof" json:"profile_id,omitempty"` + DatabaseId *string `protobuf:"bytes,10,opt,name=database_id,json=databaseId,proto3,oneof" json:"database_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -113,6 +115,20 @@ func (x *ManagedCluster) GetApiServerUrl() string { return "" } +func (x *ManagedCluster) GetProfileId() string { + if x != nil && x.ProfileId != nil { + return *x.ProfileId + } + return "" +} + +func (x *ManagedCluster) GetDatabaseId() string { + if x != nil && x.DatabaseId != nil { + return *x.DatabaseId + } + return "" +} + type CreateManagedClusterRequest struct { state protoimpl.MessageState `protogen:"open.v1"` Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` @@ -121,6 +137,8 @@ type CreateManagedClusterRequest struct { KubeconfigSecret string `protobuf:"bytes,5,opt,name=kubeconfig_secret,json=kubeconfigSecret,proto3" json:"kubeconfig_secret,omitempty"` Status *string `protobuf:"bytes,6,opt,name=status,proto3,oneof" json:"status,omitempty"` ApiServerUrl *string `protobuf:"bytes,7,opt,name=api_server_url,json=apiServerUrl,proto3,oneof" json:"api_server_url,omitempty"` + ProfileId *string `protobuf:"bytes,8,opt,name=profile_id,json=profileId,proto3,oneof" json:"profile_id,omitempty"` + DatabaseId *string `protobuf:"bytes,9,opt,name=database_id,json=databaseId,proto3,oneof" json:"database_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -197,6 +215,20 @@ func (x *CreateManagedClusterRequest) GetApiServerUrl() string { return "" } +func (x *CreateManagedClusterRequest) GetProfileId() string { + if x != nil && x.ProfileId != nil { + return *x.ProfileId + } + return "" +} + +func (x *CreateManagedClusterRequest) GetDatabaseId() string { + if x != nil && x.DatabaseId != nil { + return *x.DatabaseId + } + return "" +} + type CreateManagedClusterResponse struct { state protoimpl.MessageState `protogen:"open.v1"` ManagedCluster *ManagedCluster `protobuf:"bytes,1,opt,name=managed_cluster,json=managedCluster,proto3" json:"managed_cluster,omitempty"` @@ -338,6 +370,8 @@ type UpdateManagedClusterRequest struct { KubeconfigSecret *string `protobuf:"bytes,6,opt,name=kubeconfig_secret,json=kubeconfigSecret,proto3,oneof" json:"kubeconfig_secret,omitempty"` Status *string `protobuf:"bytes,7,opt,name=status,proto3,oneof" json:"status,omitempty"` ApiServerUrl *string `protobuf:"bytes,8,opt,name=api_server_url,json=apiServerUrl,proto3,oneof" json:"api_server_url,omitempty"` + ProfileId *string `protobuf:"bytes,9,opt,name=profile_id,json=profileId,proto3,oneof" json:"profile_id,omitempty"` + DatabaseId *string `protobuf:"bytes,10,opt,name=database_id,json=databaseId,proto3,oneof" json:"database_id,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -421,6 +455,20 @@ func (x *UpdateManagedClusterRequest) GetApiServerUrl() string { return "" } +func (x *UpdateManagedClusterRequest) GetProfileId() string { + if x != nil && x.ProfileId != nil { + return *x.ProfileId + } + return "" +} + +func (x *UpdateManagedClusterRequest) GetDatabaseId() string { + if x != nil && x.DatabaseId != nil { + return *x.DatabaseId + } + return "" +} + type UpdateManagedClusterResponse struct { state protoimpl.MessageState `protogen:"open.v1"` ManagedCluster *ManagedCluster `protobuf:"bytes,1,opt,name=managed_cluster,json=managedCluster,proto3" json:"managed_cluster,omitempty"` @@ -749,7 +797,7 @@ var File_hypershell_v1_managed_clusters_proto protoreflect.FileDescriptor const file_hypershell_v1_managed_clusters_proto_rawDesc = "" + "\n" + - "$hypershell/v1/managed_clusters.proto\x12\rhypershell.v1\x1a\x1ahypershell/v1/common.proto\"\xc7\x02\n" + + "$hypershell/v1/managed_clusters.proto\x12\rhypershell.v1\x1a\x1ahypershell/v1/common.proto\"\xb0\x03\n" + "\x0eManagedCluster\x12:\n" + "\bmetadata\x18\x01 \x01(\v2\x1e.hypershell.v1.ObjectReferenceR\bmetadata\x12\x12\n" + "\x04name\x18\x02 \x01(\tR\x04name\x12\x1a\n" + @@ -757,26 +805,39 @@ const file_hypershell_v1_managed_clusters_proto_rawDesc = "" + "\x06region\x18\x05 \x01(\tH\x00R\x06region\x88\x01\x01\x12+\n" + "\x11kubeconfig_secret\x18\x06 \x01(\tR\x10kubeconfigSecret\x12\x1b\n" + "\x06status\x18\a \x01(\tH\x01R\x06status\x88\x01\x01\x12)\n" + - "\x0eapi_server_url\x18\b \x01(\tH\x02R\fapiServerUrl\x88\x01\x01B\t\n" + + "\x0eapi_server_url\x18\b \x01(\tH\x02R\fapiServerUrl\x88\x01\x01\x12\"\n" + + "\n" + + "profile_id\x18\t \x01(\tH\x03R\tprofileId\x88\x01\x01\x12$\n" + + "\vdatabase_id\x18\n" + + " \x01(\tH\x04R\n" + + "databaseId\x88\x01\x01B\t\n" + "\a_regionB\t\n" + "\a_statusB\x11\n" + - "\x0f_api_server_urlJ\x04\b\x03\x10\x04R\bfleet_id\"\x98\x02\n" + + "\x0f_api_server_urlB\r\n" + + "\v_profile_idB\x0e\n" + + "\f_database_idJ\x04\b\x03\x10\x04R\bfleet_id\"\x81\x03\n" + "\x1bCreateManagedClusterRequest\x12\x12\n" + "\x04name\x18\x01 \x01(\tR\x04name\x12\x1a\n" + "\bprovider\x18\x03 \x01(\tR\bprovider\x12\x1b\n" + "\x06region\x18\x04 \x01(\tH\x00R\x06region\x88\x01\x01\x12+\n" + "\x11kubeconfig_secret\x18\x05 \x01(\tR\x10kubeconfigSecret\x12\x1b\n" + "\x06status\x18\x06 \x01(\tH\x01R\x06status\x88\x01\x01\x12)\n" + - "\x0eapi_server_url\x18\a \x01(\tH\x02R\fapiServerUrl\x88\x01\x01B\t\n" + + "\x0eapi_server_url\x18\a \x01(\tH\x02R\fapiServerUrl\x88\x01\x01\x12\"\n" + + "\n" + + "profile_id\x18\b \x01(\tH\x03R\tprofileId\x88\x01\x01\x12$\n" + + "\vdatabase_id\x18\t \x01(\tH\x04R\n" + + "databaseId\x88\x01\x01B\t\n" + "\a_regionB\t\n" + "\a_statusB\x11\n" + - "\x0f_api_server_urlJ\x04\b\x02\x10\x03R\bfleet_id\"f\n" + + "\x0f_api_server_urlB\r\n" + + "\v_profile_idB\x0e\n" + + "\f_database_idJ\x04\b\x02\x10\x03R\bfleet_id\"f\n" + "\x1cCreateManagedClusterResponse\x12F\n" + "\x0fmanaged_cluster\x18\x01 \x01(\v2\x1d.hypershell.v1.ManagedClusterR\x0emanagedCluster\"*\n" + "\x18GetManagedClusterRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\"c\n" + "\x19GetManagedClusterResponse\x12F\n" + - "\x0fmanaged_cluster\x18\x01 \x01(\v2\x1d.hypershell.v1.ManagedClusterR\x0emanagedCluster\"\xe3\x02\n" + + "\x0fmanaged_cluster\x18\x01 \x01(\v2\x1d.hypershell.v1.ManagedClusterR\x0emanagedCluster\"\xcc\x03\n" + "\x1bUpdateManagedClusterRequest\x12\x0e\n" + "\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" + "\x04name\x18\x02 \x01(\tH\x00R\x04name\x88\x01\x01\x12\x1f\n" + @@ -784,13 +845,20 @@ const file_hypershell_v1_managed_clusters_proto_rawDesc = "" + "\x06region\x18\x05 \x01(\tH\x02R\x06region\x88\x01\x01\x120\n" + "\x11kubeconfig_secret\x18\x06 \x01(\tH\x03R\x10kubeconfigSecret\x88\x01\x01\x12\x1b\n" + "\x06status\x18\a \x01(\tH\x04R\x06status\x88\x01\x01\x12)\n" + - "\x0eapi_server_url\x18\b \x01(\tH\x05R\fapiServerUrl\x88\x01\x01B\a\n" + + "\x0eapi_server_url\x18\b \x01(\tH\x05R\fapiServerUrl\x88\x01\x01\x12\"\n" + + "\n" + + "profile_id\x18\t \x01(\tH\x06R\tprofileId\x88\x01\x01\x12$\n" + + "\vdatabase_id\x18\n" + + " \x01(\tH\aR\n" + + "databaseId\x88\x01\x01B\a\n" + "\x05_nameB\v\n" + "\t_providerB\t\n" + "\a_regionB\x14\n" + "\x12_kubeconfig_secretB\t\n" + "\a_statusB\x11\n" + - "\x0f_api_server_urlJ\x04\b\x03\x10\x04R\bfleet_id\"f\n" + + "\x0f_api_server_urlB\r\n" + + "\v_profile_idB\x0e\n" + + "\f_database_idJ\x04\b\x03\x10\x04R\bfleet_id\"f\n" + "\x1cUpdateManagedClusterResponse\x12F\n" + "\x0fmanaged_cluster\x18\x01 \x01(\v2\x1d.hypershell.v1.ManagedClusterR\x0emanagedCluster\"-\n" + "\x1bDeleteManagedClusterRequest\x12\x0e\n" + diff --git a/components/api-server/pkg/api/openapi/.openapi-generator/FILES b/components/api-server/pkg/api/openapi/.openapi-generator/FILES index 706c9c43..205aca9c 100644 --- a/components/api-server/pkg/api/openapi/.openapi-generator/FILES +++ b/components/api-server/pkg/api/openapi/.openapi-generator/FILES @@ -15,6 +15,9 @@ docs/GatewayNetwork.md docs/GatewayNetworkList.md docs/GatewayNetworkPatchRequest.md docs/GatewayPatchRequest.md +docs/GatewayProfile.md +docs/GatewayProfileList.md +docs/GatewayProfilePatchRequest.md docs/GatewayRelease.md docs/GatewayReleaseList.md docs/GatewayReleasePatchRequest.md @@ -52,6 +55,9 @@ model_gateway_network.go model_gateway_network_list.go model_gateway_network_patch_request.go model_gateway_patch_request.go +model_gateway_profile.go +model_gateway_profile_list.go +model_gateway_profile_patch_request.go model_gateway_release.go model_gateway_release_list.go model_gateway_release_patch_request.go diff --git a/components/api-server/pkg/api/openapi/README.md b/components/api-server/pkg/api/openapi/README.md index 7a8f5d1a..f8fa9206 100644 --- a/components/api-server/pkg/api/openapi/README.md +++ b/components/api-server/pkg/api/openapi/README.md @@ -80,6 +80,7 @@ Class | Method | HTTP request | Description ------------ | ------------- | ------------- | ------------- *DefaultAPI* | [**CreateGateway**](docs/DefaultAPI.md#creategateway) | **Post** /api/hypershell/v1/gateways | Create a new gateway *DefaultAPI* | [**CreateGatewayNetwork**](docs/DefaultAPI.md#creategatewaynetwork) | **Post** /api/hypershell/v1/gateway_networks | Create a new gatewayNetwork +*DefaultAPI* | [**CreateGatewayProfile**](docs/DefaultAPI.md#creategatewayprofile) | **Post** /api/hypershell/v1/gateway_profiles | Create a new gatewayProfile *DefaultAPI* | [**CreateGatewayRelease**](docs/DefaultAPI.md#creategatewayrelease) | **Post** /api/hypershell/v1/gateway_releases | Create a new gatewayRelease *DefaultAPI* | [**CreateGatewayServiceAccount**](docs/DefaultAPI.md#creategatewayserviceaccount) | **Post** /api/hypershell/v1/gateways/{gateway_id}/service_accounts | Create an OpenShell gateway service account *DefaultAPI* | [**CreateManagedCluster**](docs/DefaultAPI.md#createmanagedcluster) | **Post** /api/hypershell/v1/managed_clusters | Create a new managedCluster @@ -87,6 +88,7 @@ Class | Method | HTTP request | Description *DefaultAPI* | [**CreateRoleBinding**](docs/DefaultAPI.md#createrolebinding) | **Post** /api/hypershell/v1/role_bindings | Create a role binding *DefaultAPI* | [**DeleteGateway**](docs/DefaultAPI.md#deletegateway) | **Delete** /api/hypershell/v1/gateways/{id} | Delete a gateway *DefaultAPI* | [**DeleteGatewayNetwork**](docs/DefaultAPI.md#deletegatewaynetwork) | **Delete** /api/hypershell/v1/gateway_networks/{id} | Delete a gateway network +*DefaultAPI* | [**DeleteGatewayProfile**](docs/DefaultAPI.md#deletegatewayprofile) | **Delete** /api/hypershell/v1/gateway_profiles/{id} | Delete a gateway profile *DefaultAPI* | [**DeleteGatewayRelease**](docs/DefaultAPI.md#deletegatewayrelease) | **Delete** /api/hypershell/v1/gateway_releases/{id} | Delete a gateway release *DefaultAPI* | [**DeleteGatewayServiceAccount**](docs/DefaultAPI.md#deletegatewayserviceaccount) | **Delete** /api/hypershell/v1/gateways/{gateway_id}/service_accounts/{service_account_id} | Delete an OpenShell gateway service account *DefaultAPI* | [**DeleteManagedCluster**](docs/DefaultAPI.md#deletemanagedcluster) | **Delete** /api/hypershell/v1/managed_clusters/{id} | Delete a managed cluster @@ -94,6 +96,7 @@ Class | Method | HTTP request | Description *DefaultAPI* | [**DeleteRoleBinding**](docs/DefaultAPI.md#deleterolebinding) | **Delete** /api/hypershell/v1/role_bindings/{id} | Delete a role binding *DefaultAPI* | [**GetGateway**](docs/DefaultAPI.md#getgateway) | **Get** /api/hypershell/v1/gateways/{id} | Get an gateway by id *DefaultAPI* | [**GetGatewayNetwork**](docs/DefaultAPI.md#getgatewaynetwork) | **Get** /api/hypershell/v1/gateway_networks/{id} | Get an gatewayNetwork by id +*DefaultAPI* | [**GetGatewayProfile**](docs/DefaultAPI.md#getgatewayprofile) | **Get** /api/hypershell/v1/gateway_profiles/{id} | Get a gatewayProfile by id *DefaultAPI* | [**GetGatewayRelease**](docs/DefaultAPI.md#getgatewayrelease) | **Get** /api/hypershell/v1/gateway_releases/{id} | Get an gatewayRelease by id *DefaultAPI* | [**GetGatewayServiceAccount**](docs/DefaultAPI.md#getgatewayserviceaccount) | **Get** /api/hypershell/v1/gateways/{gateway_id}/service_accounts/{service_account_id} | Get an OpenShell gateway service account *DefaultAPI* | [**GetManagedCluster**](docs/DefaultAPI.md#getmanagedcluster) | **Get** /api/hypershell/v1/managed_clusters/{id} | Get an managedCluster by id @@ -102,6 +105,7 @@ Class | Method | HTTP request | Description *DefaultAPI* | [**GetRole**](docs/DefaultAPI.md#getrole) | **Get** /api/hypershell/v1/roles/{id} | Get a role by ID *DefaultAPI* | [**GetRoleBinding**](docs/DefaultAPI.md#getrolebinding) | **Get** /api/hypershell/v1/role_bindings/{id} | Get a role binding by ID *DefaultAPI* | [**ListGatewayNetworks**](docs/DefaultAPI.md#listgatewaynetworks) | **Get** /api/hypershell/v1/gateway_networks | Returns a list of gatewayNetworks +*DefaultAPI* | [**ListGatewayProfiles**](docs/DefaultAPI.md#listgatewayprofiles) | **Get** /api/hypershell/v1/gateway_profiles | Returns a list of gatewayProfiles *DefaultAPI* | [**ListGatewayReleases**](docs/DefaultAPI.md#listgatewayreleases) | **Get** /api/hypershell/v1/gateway_releases | Returns a list of gatewayReleases *DefaultAPI* | [**ListGatewayServiceAccounts**](docs/DefaultAPI.md#listgatewayserviceaccounts) | **Get** /api/hypershell/v1/gateways/{gateway_id}/service_accounts | List OpenShell gateway service accounts *DefaultAPI* | [**ListGateways**](docs/DefaultAPI.md#listgateways) | **Get** /api/hypershell/v1/gateways | Returns a list of gateways @@ -112,6 +116,7 @@ Class | Method | HTTP request | Description *DefaultAPI* | [**RevokeGatewayServiceAccount**](docs/DefaultAPI.md#revokegatewayserviceaccount) | **Post** /api/hypershell/v1/gateways/{gateway_id}/service_accounts/{service_account_id}/revoke | Permanently revoke an OpenShell gateway service account *DefaultAPI* | [**UpdateGateway**](docs/DefaultAPI.md#updategateway) | **Patch** /api/hypershell/v1/gateways/{id} | Update an gateway *DefaultAPI* | [**UpdateGatewayNetwork**](docs/DefaultAPI.md#updategatewaynetwork) | **Patch** /api/hypershell/v1/gateway_networks/{id} | Update an gatewayNetwork +*DefaultAPI* | [**UpdateGatewayProfile**](docs/DefaultAPI.md#updategatewayprofile) | **Patch** /api/hypershell/v1/gateway_profiles/{id} | Update a gatewayProfile *DefaultAPI* | [**UpdateGatewayRelease**](docs/DefaultAPI.md#updategatewayrelease) | **Patch** /api/hypershell/v1/gateway_releases/{id} | Update an gatewayRelease *DefaultAPI* | [**UpdateManagedCluster**](docs/DefaultAPI.md#updatemanagedcluster) | **Patch** /api/hypershell/v1/managed_clusters/{id} | Update an managedCluster *DefaultAPI* | [**UpdateManagedDatabase**](docs/DefaultAPI.md#updatemanageddatabase) | **Patch** /api/hypershell/v1/managed_databases/{id} | Update an managedDatabase @@ -127,6 +132,9 @@ Class | Method | HTTP request | Description - [GatewayNetworkList](docs/GatewayNetworkList.md) - [GatewayNetworkPatchRequest](docs/GatewayNetworkPatchRequest.md) - [GatewayPatchRequest](docs/GatewayPatchRequest.md) + - [GatewayProfile](docs/GatewayProfile.md) + - [GatewayProfileList](docs/GatewayProfileList.md) + - [GatewayProfilePatchRequest](docs/GatewayProfilePatchRequest.md) - [GatewayRelease](docs/GatewayRelease.md) - [GatewayReleaseList](docs/GatewayReleaseList.md) - [GatewayReleasePatchRequest](docs/GatewayReleasePatchRequest.md) diff --git a/components/api-server/pkg/api/openapi/api/openapi.yaml b/components/api-server/pkg/api/openapi/api/openapi.yaml index 65e687da..23986686 100644 --- a/components/api-server/pkg/api/openapi/api/openapi.yaml +++ b/components/api-server/pkg/api/openapi/api/openapi.yaml @@ -1973,6 +1973,284 @@ paths: $ref: "#/components/schemas/Error" description: Unexpected error occurred summary: Get a role binding by ID + /api/hypershell/v1/gateway_profiles: + get: + operationId: listGatewayProfiles + parameters: + - description: Page number of record list when record list exceeds specified + page size + explode: true + in: query + name: page + required: false + schema: + default: 1 + minimum: 1 + type: integer + style: form + - description: Maximum number of records to return + explode: true + in: query + name: size + required: false + schema: + default: 100 + minimum: 0 + type: integer + style: form + - description: Specifies the search criteria + explode: true + in: query + name: search + required: false + schema: + type: string + style: form + - description: Specifies the order by criteria + explode: true + in: query + name: orderBy + required: false + schema: + type: string + style: form + - description: Supplies a comma-separated list of fields to be returned + explode: true + in: query + name: fields + required: false + schema: + type: string + style: form + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/GatewayProfileList" + description: A JSON array of gatewayProfile objects + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Auth token is invalid + "403": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Unauthorized to perform operation + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Unexpected error occurred + security: + - Bearer: [] + summary: Returns a list of gatewayProfiles + post: + operationId: createGatewayProfile + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/GatewayProfile" + description: GatewayProfile data + required: true + responses: + "201": + content: + application/json: + schema: + $ref: "#/components/schemas/GatewayProfile" + description: Created + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Validation errors occurred + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Auth token is invalid + "403": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Unauthorized to perform operation + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: GatewayProfile already exists + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: An unexpected error occurred creating the gatewayProfile + security: + - Bearer: [] + summary: Create a new gatewayProfile + /api/hypershell/v1/gateway_profiles/{id}: + delete: + operationId: deleteGatewayProfile + parameters: + - description: The id of record + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + responses: + "204": + description: Gateway profile deleted successfully + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Auth token is invalid + "403": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Unauthorized to perform operation + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: No gateway profile with specified id exists + "409": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: GatewayProfile is referenced by a cluster or gateway and cannot + be deleted + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Unexpected error deleting gateway profile + security: + - Bearer: [] + summary: Delete a gateway profile + get: + operationId: getGatewayProfile + parameters: + - description: The id of record + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/GatewayProfile" + description: GatewayProfile found by id + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Auth token is invalid + "403": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Unauthorized to perform operation + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: No gatewayProfile with specified id exists + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Unexpected error occurred + security: + - Bearer: [] + summary: Get a gatewayProfile by id + patch: + operationId: updateGatewayProfile + parameters: + - description: The id of record + explode: false + in: path + name: id + required: true + schema: + type: string + style: simple + requestBody: + content: + application/json: + schema: + $ref: "#/components/schemas/GatewayProfilePatchRequest" + description: Updated gatewayProfile data + required: true + responses: + "200": + content: + application/json: + schema: + $ref: "#/components/schemas/GatewayProfile" + description: GatewayProfile updated successfully + "400": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Validation errors occurred + "401": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Auth token is invalid + "403": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Unauthorized to perform operation + "404": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: No gatewayProfile with specified id exists + "500": + content: + application/json: + schema: + $ref: "#/components/schemas/Error" + description: Unexpected error updating gatewayProfile + security: + - Bearer: [] + summary: Update a gatewayProfile components: parameters: id: @@ -2189,17 +2467,27 @@ components: type: string api_server_url: type: string + profile_id: + description: Default GatewayProfile identifier inherited by gateways created + on this cluster when they do not supply their own profile_id + type: string + database_id: + description: Default ManagedDatabase identifier for gateways placed on + this cluster + type: string required: - kubeconfig_secret - name - provider type: object example: + kind: kind + created_at: 2000-01-23T04:56:07.000+00:00 updated_at: 2000-01-23T04:56:07.000+00:00 provider: provider - kind: kind + database_id: database_id + profile_id: profile_id name: name - created_at: 2000-01-23T04:56:07.000+00:00 kubeconfig_secret: kubeconfig_secret id: id href: href @@ -2225,22 +2513,26 @@ components: id: id href: href items: - - updated_at: 2000-01-23T04:56:07.000+00:00 + - kind: kind + created_at: 2000-01-23T04:56:07.000+00:00 + updated_at: 2000-01-23T04:56:07.000+00:00 provider: provider - kind: kind + database_id: database_id + profile_id: profile_id name: name - created_at: 2000-01-23T04:56:07.000+00:00 kubeconfig_secret: kubeconfig_secret id: id href: href region: region status: status api_server_url: api_server_url - - updated_at: 2000-01-23T04:56:07.000+00:00 + - kind: kind + created_at: 2000-01-23T04:56:07.000+00:00 + updated_at: 2000-01-23T04:56:07.000+00:00 provider: provider - kind: kind + database_id: database_id + profile_id: profile_id name: name - created_at: 2000-01-23T04:56:07.000+00:00 kubeconfig_secret: kubeconfig_secret id: id href: href @@ -2250,6 +2542,8 @@ components: ManagedClusterPatchRequest: example: provider: provider + database_id: database_id + profile_id: profile_id name: name kubeconfig_secret: kubeconfig_secret region: region @@ -2268,6 +2562,12 @@ components: type: string api_server_url: type: string + profile_id: + description: Default GatewayProfile for gateways on this cluster; set to + empty string to clear + type: string + database_id: + type: string type: object ManagedDatabase: allOf: @@ -2493,6 +2793,11 @@ components: type: string release_id: type: string + profile_id: + description: GatewayProfile identifier used for namespace quota enforcement; + required at creation (client-supplied or inherited from the cluster + default) and reassignable but not clearable afterward + type: string database_id: description: Server-assigned ManagedDatabase identifier; client-supplied values are ignored @@ -2585,6 +2890,7 @@ components: tls_mode: tls_mode route: route database_id: database_id + profile_id: profile_id name: name namespace: namespace status: status @@ -2631,6 +2937,7 @@ components: tls_mode: tls_mode route: route database_id: database_id + profile_id: profile_id name: name namespace: namespace status: status @@ -2658,6 +2965,7 @@ components: tls_mode: tls_mode route: route database_id: database_id + profile_id: profile_id name: name namespace: namespace status: status @@ -2678,6 +2986,7 @@ components: tls_mode: tls_mode route: route database_id: database_id + profile_id: profile_id name: name external_dns: external_dns status: status @@ -2688,6 +2997,10 @@ components: type: string release_id: type: string + profile_id: + description: Reassign the gateway to a different GatewayProfile; cannot + be set to null or empty (HTTP 400) and the target profile must exist + type: string database_id: description: Server-owned placement field; values supplied through PATCH are ignored @@ -3332,6 +3645,173 @@ components: id: id href: href gateway_id: gateway_id + GatewayProfile: + allOf: + - $ref: "#/components/schemas/ObjectReference" + - properties: + name: + type: string + description: + description: Profile purpose/intent + type: string + cpu_request_total: + description: "Total CPU requests allowed (e.g., \"4\", \"500m\")" + type: string + cpu_limit_total: + description: Total CPU limits allowed + type: string + memory_request_total: + description: "Total memory requests allowed (e.g., \"8Gi\", \"512Mi\")" + type: string + memory_limit_total: + description: Total memory limits allowed + type: string + ephemeral_storage_total: + description: "Total ephemeral storage allowed (e.g., \"10Gi\")" + type: string + pod_count: + description: Maximum number of pods (0 means not set) + format: int32 + type: integer + pvc_count: + description: Maximum number of PersistentVolumeClaims (0 means not set) + format: int32 + type: integer + container_cpu_request_default: + description: Default CPU request injected into containers without explicit + request + type: string + container_cpu_limit_max: + description: Maximum CPU limit a container can request + type: string + container_memory_request_default: + description: Default memory request injected into containers + type: string + container_memory_limit_max: + description: Maximum memory limit a container can request + type: string + required: + - name + type: object + example: + ephemeral_storage_total: ephemeral_storage_total + kind: kind + container_cpu_request_default: container_cpu_request_default + created_at: 2000-01-23T04:56:07.000+00:00 + description: description + cpu_request_total: cpu_request_total + pvc_count: 5 + container_memory_limit_max: container_memory_limit_max + memory_request_total: memory_request_total + pod_count: 5 + updated_at: 2000-01-23T04:56:07.000+00:00 + name: name + cpu_limit_total: cpu_limit_total + container_cpu_limit_max: container_cpu_limit_max + container_memory_request_default: container_memory_request_default + id: id + href: href + memory_limit_total: memory_limit_total + GatewayProfileList: + allOf: + - $ref: "#/components/schemas/List" + - properties: + items: + items: + $ref: "#/components/schemas/GatewayProfile" + type: array + type: object + example: + total: 1 + size: 6 + updated_at: 2000-01-23T04:56:07.000+00:00 + kind: kind + created_at: 2000-01-23T04:56:07.000+00:00 + page: 0 + id: id + href: href + items: + - ephemeral_storage_total: ephemeral_storage_total + kind: kind + container_cpu_request_default: container_cpu_request_default + created_at: 2000-01-23T04:56:07.000+00:00 + description: description + cpu_request_total: cpu_request_total + pvc_count: 5 + container_memory_limit_max: container_memory_limit_max + memory_request_total: memory_request_total + pod_count: 5 + updated_at: 2000-01-23T04:56:07.000+00:00 + name: name + cpu_limit_total: cpu_limit_total + container_cpu_limit_max: container_cpu_limit_max + container_memory_request_default: container_memory_request_default + id: id + href: href + memory_limit_total: memory_limit_total + - ephemeral_storage_total: ephemeral_storage_total + kind: kind + container_cpu_request_default: container_cpu_request_default + created_at: 2000-01-23T04:56:07.000+00:00 + description: description + cpu_request_total: cpu_request_total + pvc_count: 5 + container_memory_limit_max: container_memory_limit_max + memory_request_total: memory_request_total + pod_count: 5 + updated_at: 2000-01-23T04:56:07.000+00:00 + name: name + cpu_limit_total: cpu_limit_total + container_cpu_limit_max: container_cpu_limit_max + container_memory_request_default: container_memory_request_default + id: id + href: href + memory_limit_total: memory_limit_total + GatewayProfilePatchRequest: + example: + ephemeral_storage_total: ephemeral_storage_total + container_cpu_request_default: container_cpu_request_default + description: description + cpu_request_total: cpu_request_total + pvc_count: 6 + container_memory_limit_max: container_memory_limit_max + memory_request_total: memory_request_total + pod_count: 0 + name: name + cpu_limit_total: cpu_limit_total + container_cpu_limit_max: container_cpu_limit_max + container_memory_request_default: container_memory_request_default + memory_limit_total: memory_limit_total + properties: + name: + type: string + description: + type: string + cpu_request_total: + type: string + cpu_limit_total: + type: string + memory_request_total: + type: string + memory_limit_total: + type: string + ephemeral_storage_total: + type: string + pod_count: + format: int32 + type: integer + pvc_count: + format: int32 + type: integer + container_cpu_request_default: + type: string + container_cpu_limit_max: + type: string + container_memory_request_default: + type: string + container_memory_limit_max: + type: string + type: object GatewayCreateRequest: example: phase: phase @@ -3348,6 +3828,7 @@ components: tls_mode: tls_mode route: route database_id: database_id + profile_id: profile_id name: name external_dns: external_dns status: status @@ -3358,6 +3839,11 @@ components: type: string release_id: type: string + profile_id: + description: "Optional GatewayProfile identifier for quota enforcement;\ + \ if omitted the API server falls back to the cluster's default profile,\ + \ and rejects the request with HTTP 400 if neither is provided" + type: string database_id: description: Required placement placeholder; the API server ignores its value and assigns the ManagedDatabase diff --git a/components/api-server/pkg/api/openapi/api_default.go b/components/api-server/pkg/api/openapi/api_default.go index 7621d56d..03e94b0a 100644 --- a/components/api-server/pkg/api/openapi/api_default.go +++ b/components/api-server/pkg/api/openapi/api_default.go @@ -350,6 +350,170 @@ func (a *DefaultAPIService) CreateGatewayNetworkExecute(r ApiCreateGatewayNetwor return localVarReturnValue, localVarHTTPResponse, nil } +type ApiCreateGatewayProfileRequest struct { + ctx context.Context + ApiService *DefaultAPIService + gatewayProfile *GatewayProfile +} + +// GatewayProfile data +func (r ApiCreateGatewayProfileRequest) GatewayProfile(gatewayProfile GatewayProfile) ApiCreateGatewayProfileRequest { + r.gatewayProfile = &gatewayProfile + return r +} + +func (r ApiCreateGatewayProfileRequest) Execute() (*GatewayProfile, *http.Response, error) { + return r.ApiService.CreateGatewayProfileExecute(r) +} + +/* +CreateGatewayProfile Create a new gatewayProfile + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiCreateGatewayProfileRequest +*/ +func (a *DefaultAPIService) CreateGatewayProfile(ctx context.Context) ApiCreateGatewayProfileRequest { + return ApiCreateGatewayProfileRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// +// @return GatewayProfile +func (a *DefaultAPIService) CreateGatewayProfileExecute(r ApiCreateGatewayProfileRequest) (*GatewayProfile, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *GatewayProfile + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.CreateGatewayProfile") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/hypershell/v1/gateway_profiles" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.gatewayProfile == nil { + return localVarReturnValue, nil, reportError("gatewayProfile is required and must be specified") + } + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.gatewayProfile + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 401 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 409 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiCreateGatewayReleaseRequest struct { ctx context.Context ApiService *DefaultAPIService @@ -1417,25 +1581,25 @@ func (a *DefaultAPIService) DeleteGatewayNetworkExecute(r ApiDeleteGatewayNetwor return localVarHTTPResponse, nil } -type ApiDeleteGatewayReleaseRequest struct { +type ApiDeleteGatewayProfileRequest struct { ctx context.Context ApiService *DefaultAPIService id string } -func (r ApiDeleteGatewayReleaseRequest) Execute() (*http.Response, error) { - return r.ApiService.DeleteGatewayReleaseExecute(r) +func (r ApiDeleteGatewayProfileRequest) Execute() (*http.Response, error) { + return r.ApiService.DeleteGatewayProfileExecute(r) } /* -DeleteGatewayRelease Delete a gateway release +DeleteGatewayProfile Delete a gateway profile @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). @param id The id of record - @return ApiDeleteGatewayReleaseRequest + @return ApiDeleteGatewayProfileRequest */ -func (a *DefaultAPIService) DeleteGatewayRelease(ctx context.Context, id string) ApiDeleteGatewayReleaseRequest { - return ApiDeleteGatewayReleaseRequest{ +func (a *DefaultAPIService) DeleteGatewayProfile(ctx context.Context, id string) ApiDeleteGatewayProfileRequest { + return ApiDeleteGatewayProfileRequest{ ApiService: a, ctx: ctx, id: id, @@ -1443,19 +1607,19 @@ func (a *DefaultAPIService) DeleteGatewayRelease(ctx context.Context, id string) } // Execute executes the request -func (a *DefaultAPIService) DeleteGatewayReleaseExecute(r ApiDeleteGatewayReleaseRequest) (*http.Response, error) { +func (a *DefaultAPIService) DeleteGatewayProfileExecute(r ApiDeleteGatewayProfileRequest) (*http.Response, error) { var ( localVarHTTPMethod = http.MethodDelete localVarPostBody interface{} formFiles []formFile ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.DeleteGatewayRelease") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.DeleteGatewayProfile") if err != nil { return nil, &GenericOpenAPIError{error: err.Error()} } - localVarPath := localBasePath + "/api/hypershell/v1/gateway_releases/{id}" + localVarPath := localBasePath + "/api/hypershell/v1/gateway_profiles/{id}" localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1) localVarHeaderParams := make(map[string]string) @@ -1534,6 +1698,17 @@ func (a *DefaultAPIService) DeleteGatewayReleaseExecute(r ApiDeleteGatewayReleas newErr.model = v return localVarHTTPResponse, newErr } + if localVarHTTPResponse.StatusCode == 409 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarHTTPResponse, newErr + } if localVarHTTPResponse.StatusCode == 500 { var v Error err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) @@ -1550,53 +1725,46 @@ func (a *DefaultAPIService) DeleteGatewayReleaseExecute(r ApiDeleteGatewayReleas return localVarHTTPResponse, nil } -type ApiDeleteGatewayServiceAccountRequest struct { - ctx context.Context - ApiService *DefaultAPIService - gatewayId string - serviceAccountId string +type ApiDeleteGatewayReleaseRequest struct { + ctx context.Context + ApiService *DefaultAPIService + id string } -func (r ApiDeleteGatewayServiceAccountRequest) Execute() (*OpenShellGatewayServiceAccountListItem, *http.Response, error) { - return r.ApiService.DeleteGatewayServiceAccountExecute(r) +func (r ApiDeleteGatewayReleaseRequest) Execute() (*http.Response, error) { + return r.ApiService.DeleteGatewayReleaseExecute(r) } /* -DeleteGatewayServiceAccount Delete an OpenShell gateway service account +DeleteGatewayRelease Delete a gateway release @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param gatewayId Selected Gateway ID - @param serviceAccountId OpenShellGatewayServiceAccount ID - @return ApiDeleteGatewayServiceAccountRequest + @param id The id of record + @return ApiDeleteGatewayReleaseRequest */ -func (a *DefaultAPIService) DeleteGatewayServiceAccount(ctx context.Context, gatewayId string, serviceAccountId string) ApiDeleteGatewayServiceAccountRequest { - return ApiDeleteGatewayServiceAccountRequest{ - ApiService: a, - ctx: ctx, - gatewayId: gatewayId, - serviceAccountId: serviceAccountId, +func (a *DefaultAPIService) DeleteGatewayRelease(ctx context.Context, id string) ApiDeleteGatewayReleaseRequest { + return ApiDeleteGatewayReleaseRequest{ + ApiService: a, + ctx: ctx, + id: id, } } // Execute executes the request -// -// @return OpenShellGatewayServiceAccountListItem -func (a *DefaultAPIService) DeleteGatewayServiceAccountExecute(r ApiDeleteGatewayServiceAccountRequest) (*OpenShellGatewayServiceAccountListItem, *http.Response, error) { +func (a *DefaultAPIService) DeleteGatewayReleaseExecute(r ApiDeleteGatewayReleaseRequest) (*http.Response, error) { var ( - localVarHTTPMethod = http.MethodDelete - localVarPostBody interface{} - formFiles []formFile - localVarReturnValue *OpenShellGatewayServiceAccountListItem + localVarHTTPMethod = http.MethodDelete + localVarPostBody interface{} + formFiles []formFile ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.DeleteGatewayServiceAccount") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.DeleteGatewayRelease") if err != nil { - return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + return nil, &GenericOpenAPIError{error: err.Error()} } - localVarPath := localBasePath + "/api/hypershell/v1/gateways/{gateway_id}/service_accounts/{service_account_id}" - localVarPath = strings.Replace(localVarPath, "{"+"gateway_id"+"}", url.PathEscape(parameterValueToString(r.gatewayId, "gatewayId")), -1) - localVarPath = strings.Replace(localVarPath, "{"+"service_account_id"+"}", url.PathEscape(parameterValueToString(r.serviceAccountId, "serviceAccountId")), -1) + localVarPath := localBasePath + "/api/hypershell/v1/gateway_releases/{id}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1) localVarHeaderParams := make(map[string]string) localVarQueryParams := url.Values{} @@ -1621,19 +1789,19 @@ func (a *DefaultAPIService) DeleteGatewayServiceAccountExecute(r ApiDeleteGatewa } req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) if err != nil { - return localVarReturnValue, nil, err + return nil, err } localVarHTTPResponse, err := a.client.callAPI(req) if err != nil || localVarHTTPResponse == nil { - return localVarReturnValue, localVarHTTPResponse, err + return localVarHTTPResponse, err } localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) localVarHTTPResponse.Body.Close() localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) if err != nil { - return localVarReturnValue, localVarHTTPResponse, err + return localVarHTTPResponse, err } if localVarHTTPResponse.StatusCode >= 300 { @@ -1641,31 +1809,171 @@ func (a *DefaultAPIService) DeleteGatewayServiceAccountExecute(r ApiDeleteGatewa body: localVarBody, error: localVarHTTPResponse.Status, } + if localVarHTTPResponse.StatusCode == 401 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarHTTPResponse, newErr + } if localVarHTTPResponse.StatusCode == 404 { var v Error err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr + return localVarHTTPResponse, newErr } newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) newErr.model = v - return localVarReturnValue, localVarHTTPResponse, newErr + return localVarHTTPResponse, newErr } - if localVarHTTPResponse.StatusCode == 503 { + if localVarHTTPResponse.StatusCode == 500 { var v Error err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { newErr.error = err.Error() - return localVarReturnValue, localVarHTTPResponse, newErr + return localVarHTTPResponse, newErr } newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) newErr.model = v } - return localVarReturnValue, localVarHTTPResponse, newErr + return localVarHTTPResponse, newErr } - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + return localVarHTTPResponse, nil +} + +type ApiDeleteGatewayServiceAccountRequest struct { + ctx context.Context + ApiService *DefaultAPIService + gatewayId string + serviceAccountId string +} + +func (r ApiDeleteGatewayServiceAccountRequest) Execute() (*OpenShellGatewayServiceAccountListItem, *http.Response, error) { + return r.ApiService.DeleteGatewayServiceAccountExecute(r) +} + +/* +DeleteGatewayServiceAccount Delete an OpenShell gateway service account + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param gatewayId Selected Gateway ID + @param serviceAccountId OpenShellGatewayServiceAccount ID + @return ApiDeleteGatewayServiceAccountRequest +*/ +func (a *DefaultAPIService) DeleteGatewayServiceAccount(ctx context.Context, gatewayId string, serviceAccountId string) ApiDeleteGatewayServiceAccountRequest { + return ApiDeleteGatewayServiceAccountRequest{ + ApiService: a, + ctx: ctx, + gatewayId: gatewayId, + serviceAccountId: serviceAccountId, + } +} + +// Execute executes the request +// +// @return OpenShellGatewayServiceAccountListItem +func (a *DefaultAPIService) DeleteGatewayServiceAccountExecute(r ApiDeleteGatewayServiceAccountRequest) (*OpenShellGatewayServiceAccountListItem, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodDelete + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *OpenShellGatewayServiceAccountListItem + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.DeleteGatewayServiceAccount") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/hypershell/v1/gateways/{gateway_id}/service_accounts/{service_account_id}" + localVarPath = strings.Replace(localVarPath, "{"+"gateway_id"+"}", url.PathEscape(parameterValueToString(r.gatewayId, "gatewayId")), -1) + localVarPath = strings.Replace(localVarPath, "{"+"service_account_id"+"}", url.PathEscape(parameterValueToString(r.serviceAccountId, "serviceAccountId")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 404 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 503 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { newErr := &GenericOpenAPIError{ body: localVarBody, @@ -2355,6 +2663,151 @@ func (a *DefaultAPIService) GetGatewayNetworkExecute(r ApiGetGatewayNetworkReque return localVarReturnValue, localVarHTTPResponse, nil } +type ApiGetGatewayProfileRequest struct { + ctx context.Context + ApiService *DefaultAPIService + id string +} + +func (r ApiGetGatewayProfileRequest) Execute() (*GatewayProfile, *http.Response, error) { + return r.ApiService.GetGatewayProfileExecute(r) +} + +/* +GetGatewayProfile Get a gatewayProfile by id + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param id The id of record + @return ApiGetGatewayProfileRequest +*/ +func (a *DefaultAPIService) GetGatewayProfile(ctx context.Context, id string) ApiGetGatewayProfileRequest { + return ApiGetGatewayProfileRequest{ + ApiService: a, + ctx: ctx, + id: id, + } +} + +// Execute executes the request +// +// @return GatewayProfile +func (a *DefaultAPIService) GetGatewayProfileExecute(r ApiGetGatewayProfileRequest) (*GatewayProfile, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *GatewayProfile + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.GetGatewayProfile") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/hypershell/v1/gateway_profiles/{id}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 401 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 404 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiGetGatewayReleaseRequest struct { ctx context.Context ApiService *DefaultAPIService @@ -3126,66 +3579,241 @@ func (a *DefaultAPIService) GetRoleExecute(r ApiGetRoleRequest) (*Role, *http.Re return localVarReturnValue, localVarHTTPResponse, newErr } - err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) - if err != nil { - newErr := &GenericOpenAPIError{ - body: localVarBody, - error: err.Error(), - } - return localVarReturnValue, localVarHTTPResponse, newErr - } + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiGetRoleBindingRequest struct { + ctx context.Context + ApiService *DefaultAPIService + id string +} + +func (r ApiGetRoleBindingRequest) Execute() (*RoleBinding, *http.Response, error) { + return r.ApiService.GetRoleBindingExecute(r) +} + +/* +GetRoleBinding Get a role binding by ID + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param id The id of record + @return ApiGetRoleBindingRequest +*/ +func (a *DefaultAPIService) GetRoleBinding(ctx context.Context, id string) ApiGetRoleBindingRequest { + return ApiGetRoleBindingRequest{ + ApiService: a, + ctx: ctx, + id: id, + } +} + +// Execute executes the request +// +// @return RoleBinding +func (a *DefaultAPIService) GetRoleBindingExecute(r ApiGetRoleBindingRequest) (*RoleBinding, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodGet + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *RoleBinding + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.GetRoleBinding") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/hypershell/v1/role_bindings/{id}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 404 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + +type ApiListGatewayNetworksRequest struct { + ctx context.Context + ApiService *DefaultAPIService + page *int32 + size *int32 + search *string + orderBy *string + fields *string +} + +// Page number of record list when record list exceeds specified page size +func (r ApiListGatewayNetworksRequest) Page(page int32) ApiListGatewayNetworksRequest { + r.page = &page + return r +} + +// Maximum number of records to return +func (r ApiListGatewayNetworksRequest) Size(size int32) ApiListGatewayNetworksRequest { + r.size = &size + return r +} + +// Specifies the search criteria +func (r ApiListGatewayNetworksRequest) Search(search string) ApiListGatewayNetworksRequest { + r.search = &search + return r +} - return localVarReturnValue, localVarHTTPResponse, nil +// Specifies the order by criteria +func (r ApiListGatewayNetworksRequest) OrderBy(orderBy string) ApiListGatewayNetworksRequest { + r.orderBy = &orderBy + return r } -type ApiGetRoleBindingRequest struct { - ctx context.Context - ApiService *DefaultAPIService - id string +// Supplies a comma-separated list of fields to be returned +func (r ApiListGatewayNetworksRequest) Fields(fields string) ApiListGatewayNetworksRequest { + r.fields = &fields + return r } -func (r ApiGetRoleBindingRequest) Execute() (*RoleBinding, *http.Response, error) { - return r.ApiService.GetRoleBindingExecute(r) +func (r ApiListGatewayNetworksRequest) Execute() (*GatewayNetworkList, *http.Response, error) { + return r.ApiService.ListGatewayNetworksExecute(r) } /* -GetRoleBinding Get a role binding by ID +ListGatewayNetworks Returns a list of gatewayNetworks @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @param id The id of record - @return ApiGetRoleBindingRequest + @return ApiListGatewayNetworksRequest */ -func (a *DefaultAPIService) GetRoleBinding(ctx context.Context, id string) ApiGetRoleBindingRequest { - return ApiGetRoleBindingRequest{ +func (a *DefaultAPIService) ListGatewayNetworks(ctx context.Context) ApiListGatewayNetworksRequest { + return ApiListGatewayNetworksRequest{ ApiService: a, ctx: ctx, - id: id, } } // Execute executes the request // -// @return RoleBinding -func (a *DefaultAPIService) GetRoleBindingExecute(r ApiGetRoleBindingRequest) (*RoleBinding, *http.Response, error) { +// @return GatewayNetworkList +func (a *DefaultAPIService) ListGatewayNetworksExecute(r ApiListGatewayNetworksRequest) (*GatewayNetworkList, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} formFiles []formFile - localVarReturnValue *RoleBinding + localVarReturnValue *GatewayNetworkList ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.GetRoleBinding") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.ListGatewayNetworks") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } - localVarPath := localBasePath + "/api/hypershell/v1/role_bindings/{id}" - localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1) + localVarPath := localBasePath + "/api/hypershell/v1/gateway_networks" localVarHeaderParams := make(map[string]string) localVarQueryParams := url.Values{} localVarFormParams := url.Values{} + if r.page != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "page", r.page, "form", "") + } else { + var defaultValue int32 = 1 + r.page = &defaultValue + } + if r.size != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "size", r.size, "form", "") + } else { + var defaultValue int32 = 100 + r.size = &defaultValue + } + if r.search != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "search", r.search, "form", "") + } + if r.orderBy != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "orderBy", r.orderBy, "form", "") + } + if r.fields != nil { + parameterAddToHeaderOrQuery(localVarQueryParams, "fields", r.fields, "form", "") + } // to determine the Content-Type header localVarHTTPContentTypes := []string{} @@ -3225,7 +3853,18 @@ func (a *DefaultAPIService) GetRoleBindingExecute(r ApiGetRoleBindingRequest) (* body: localVarBody, error: localVarHTTPResponse.Status, } - if localVarHTTPResponse.StatusCode == 404 { + if localVarHTTPResponse.StatusCode == 401 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { var v Error err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) if err != nil { @@ -3261,7 +3900,7 @@ func (a *DefaultAPIService) GetRoleBindingExecute(r ApiGetRoleBindingRequest) (* return localVarReturnValue, localVarHTTPResponse, nil } -type ApiListGatewayNetworksRequest struct { +type ApiListGatewayProfilesRequest struct { ctx context.Context ApiService *DefaultAPIService page *int32 @@ -3272,47 +3911,47 @@ type ApiListGatewayNetworksRequest struct { } // Page number of record list when record list exceeds specified page size -func (r ApiListGatewayNetworksRequest) Page(page int32) ApiListGatewayNetworksRequest { +func (r ApiListGatewayProfilesRequest) Page(page int32) ApiListGatewayProfilesRequest { r.page = &page return r } // Maximum number of records to return -func (r ApiListGatewayNetworksRequest) Size(size int32) ApiListGatewayNetworksRequest { +func (r ApiListGatewayProfilesRequest) Size(size int32) ApiListGatewayProfilesRequest { r.size = &size return r } // Specifies the search criteria -func (r ApiListGatewayNetworksRequest) Search(search string) ApiListGatewayNetworksRequest { +func (r ApiListGatewayProfilesRequest) Search(search string) ApiListGatewayProfilesRequest { r.search = &search return r } // Specifies the order by criteria -func (r ApiListGatewayNetworksRequest) OrderBy(orderBy string) ApiListGatewayNetworksRequest { +func (r ApiListGatewayProfilesRequest) OrderBy(orderBy string) ApiListGatewayProfilesRequest { r.orderBy = &orderBy return r } // Supplies a comma-separated list of fields to be returned -func (r ApiListGatewayNetworksRequest) Fields(fields string) ApiListGatewayNetworksRequest { +func (r ApiListGatewayProfilesRequest) Fields(fields string) ApiListGatewayProfilesRequest { r.fields = &fields return r } -func (r ApiListGatewayNetworksRequest) Execute() (*GatewayNetworkList, *http.Response, error) { - return r.ApiService.ListGatewayNetworksExecute(r) +func (r ApiListGatewayProfilesRequest) Execute() (*GatewayProfileList, *http.Response, error) { + return r.ApiService.ListGatewayProfilesExecute(r) } /* -ListGatewayNetworks Returns a list of gatewayNetworks +ListGatewayProfiles Returns a list of gatewayProfiles @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). - @return ApiListGatewayNetworksRequest + @return ApiListGatewayProfilesRequest */ -func (a *DefaultAPIService) ListGatewayNetworks(ctx context.Context) ApiListGatewayNetworksRequest { - return ApiListGatewayNetworksRequest{ +func (a *DefaultAPIService) ListGatewayProfiles(ctx context.Context) ApiListGatewayProfilesRequest { + return ApiListGatewayProfilesRequest{ ApiService: a, ctx: ctx, } @@ -3320,21 +3959,21 @@ func (a *DefaultAPIService) ListGatewayNetworks(ctx context.Context) ApiListGate // Execute executes the request // -// @return GatewayNetworkList -func (a *DefaultAPIService) ListGatewayNetworksExecute(r ApiListGatewayNetworksRequest) (*GatewayNetworkList, *http.Response, error) { +// @return GatewayProfileList +func (a *DefaultAPIService) ListGatewayProfilesExecute(r ApiListGatewayProfilesRequest) (*GatewayProfileList, *http.Response, error) { var ( localVarHTTPMethod = http.MethodGet localVarPostBody interface{} formFiles []formFile - localVarReturnValue *GatewayNetworkList + localVarReturnValue *GatewayProfileList ) - localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.ListGatewayNetworks") + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.ListGatewayProfiles") if err != nil { return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} } - localVarPath := localBasePath + "/api/hypershell/v1/gateway_networks" + localVarPath := localBasePath + "/api/hypershell/v1/gateway_profiles" localVarHeaderParams := make(map[string]string) localVarQueryParams := url.Values{} @@ -5207,6 +5846,174 @@ func (a *DefaultAPIService) UpdateGatewayNetworkExecute(r ApiUpdateGatewayNetwor return localVarReturnValue, localVarHTTPResponse, nil } +type ApiUpdateGatewayProfileRequest struct { + ctx context.Context + ApiService *DefaultAPIService + id string + gatewayProfilePatchRequest *GatewayProfilePatchRequest +} + +// Updated gatewayProfile data +func (r ApiUpdateGatewayProfileRequest) GatewayProfilePatchRequest(gatewayProfilePatchRequest GatewayProfilePatchRequest) ApiUpdateGatewayProfileRequest { + r.gatewayProfilePatchRequest = &gatewayProfilePatchRequest + return r +} + +func (r ApiUpdateGatewayProfileRequest) Execute() (*GatewayProfile, *http.Response, error) { + return r.ApiService.UpdateGatewayProfileExecute(r) +} + +/* +UpdateGatewayProfile Update a gatewayProfile + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @param id The id of record + @return ApiUpdateGatewayProfileRequest +*/ +func (a *DefaultAPIService) UpdateGatewayProfile(ctx context.Context, id string) ApiUpdateGatewayProfileRequest { + return ApiUpdateGatewayProfileRequest{ + ApiService: a, + ctx: ctx, + id: id, + } +} + +// Execute executes the request +// +// @return GatewayProfile +func (a *DefaultAPIService) UpdateGatewayProfileExecute(r ApiUpdateGatewayProfileRequest) (*GatewayProfile, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPatch + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *GatewayProfile + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "DefaultAPIService.UpdateGatewayProfile") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/api/hypershell/v1/gateway_profiles/{id}" + localVarPath = strings.Replace(localVarPath, "{"+"id"+"}", url.PathEscape(parameterValueToString(r.id, "id")), -1) + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.gatewayProfilePatchRequest == nil { + return localVarReturnValue, nil, reportError("gatewayProfilePatchRequest is required and must be specified") + } + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.gatewayProfilePatchRequest + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + if localVarHTTPResponse.StatusCode == 400 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 401 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 403 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 404 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + if localVarHTTPResponse.StatusCode == 500 { + var v Error + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} + type ApiUpdateGatewayReleaseRequest struct { ctx context.Context ApiService *DefaultAPIService diff --git a/components/api-server/pkg/api/openapi/docs/DefaultAPI.md b/components/api-server/pkg/api/openapi/docs/DefaultAPI.md index 2a96f7a7..1d456d3e 100644 --- a/components/api-server/pkg/api/openapi/docs/DefaultAPI.md +++ b/components/api-server/pkg/api/openapi/docs/DefaultAPI.md @@ -6,6 +6,7 @@ Method | HTTP request | Description ------------- | ------------- | ------------- [**CreateGateway**](DefaultAPI.md#CreateGateway) | **Post** /api/hypershell/v1/gateways | Create a new gateway [**CreateGatewayNetwork**](DefaultAPI.md#CreateGatewayNetwork) | **Post** /api/hypershell/v1/gateway_networks | Create a new gatewayNetwork +[**CreateGatewayProfile**](DefaultAPI.md#CreateGatewayProfile) | **Post** /api/hypershell/v1/gateway_profiles | Create a new gatewayProfile [**CreateGatewayRelease**](DefaultAPI.md#CreateGatewayRelease) | **Post** /api/hypershell/v1/gateway_releases | Create a new gatewayRelease [**CreateGatewayServiceAccount**](DefaultAPI.md#CreateGatewayServiceAccount) | **Post** /api/hypershell/v1/gateways/{gateway_id}/service_accounts | Create an OpenShell gateway service account [**CreateManagedCluster**](DefaultAPI.md#CreateManagedCluster) | **Post** /api/hypershell/v1/managed_clusters | Create a new managedCluster @@ -13,6 +14,7 @@ Method | HTTP request | Description [**CreateRoleBinding**](DefaultAPI.md#CreateRoleBinding) | **Post** /api/hypershell/v1/role_bindings | Create a role binding [**DeleteGateway**](DefaultAPI.md#DeleteGateway) | **Delete** /api/hypershell/v1/gateways/{id} | Delete a gateway [**DeleteGatewayNetwork**](DefaultAPI.md#DeleteGatewayNetwork) | **Delete** /api/hypershell/v1/gateway_networks/{id} | Delete a gateway network +[**DeleteGatewayProfile**](DefaultAPI.md#DeleteGatewayProfile) | **Delete** /api/hypershell/v1/gateway_profiles/{id} | Delete a gateway profile [**DeleteGatewayRelease**](DefaultAPI.md#DeleteGatewayRelease) | **Delete** /api/hypershell/v1/gateway_releases/{id} | Delete a gateway release [**DeleteGatewayServiceAccount**](DefaultAPI.md#DeleteGatewayServiceAccount) | **Delete** /api/hypershell/v1/gateways/{gateway_id}/service_accounts/{service_account_id} | Delete an OpenShell gateway service account [**DeleteManagedCluster**](DefaultAPI.md#DeleteManagedCluster) | **Delete** /api/hypershell/v1/managed_clusters/{id} | Delete a managed cluster @@ -20,6 +22,7 @@ Method | HTTP request | Description [**DeleteRoleBinding**](DefaultAPI.md#DeleteRoleBinding) | **Delete** /api/hypershell/v1/role_bindings/{id} | Delete a role binding [**GetGateway**](DefaultAPI.md#GetGateway) | **Get** /api/hypershell/v1/gateways/{id} | Get an gateway by id [**GetGatewayNetwork**](DefaultAPI.md#GetGatewayNetwork) | **Get** /api/hypershell/v1/gateway_networks/{id} | Get an gatewayNetwork by id +[**GetGatewayProfile**](DefaultAPI.md#GetGatewayProfile) | **Get** /api/hypershell/v1/gateway_profiles/{id} | Get a gatewayProfile by id [**GetGatewayRelease**](DefaultAPI.md#GetGatewayRelease) | **Get** /api/hypershell/v1/gateway_releases/{id} | Get an gatewayRelease by id [**GetGatewayServiceAccount**](DefaultAPI.md#GetGatewayServiceAccount) | **Get** /api/hypershell/v1/gateways/{gateway_id}/service_accounts/{service_account_id} | Get an OpenShell gateway service account [**GetManagedCluster**](DefaultAPI.md#GetManagedCluster) | **Get** /api/hypershell/v1/managed_clusters/{id} | Get an managedCluster by id @@ -28,6 +31,7 @@ Method | HTTP request | Description [**GetRole**](DefaultAPI.md#GetRole) | **Get** /api/hypershell/v1/roles/{id} | Get a role by ID [**GetRoleBinding**](DefaultAPI.md#GetRoleBinding) | **Get** /api/hypershell/v1/role_bindings/{id} | Get a role binding by ID [**ListGatewayNetworks**](DefaultAPI.md#ListGatewayNetworks) | **Get** /api/hypershell/v1/gateway_networks | Returns a list of gatewayNetworks +[**ListGatewayProfiles**](DefaultAPI.md#ListGatewayProfiles) | **Get** /api/hypershell/v1/gateway_profiles | Returns a list of gatewayProfiles [**ListGatewayReleases**](DefaultAPI.md#ListGatewayReleases) | **Get** /api/hypershell/v1/gateway_releases | Returns a list of gatewayReleases [**ListGatewayServiceAccounts**](DefaultAPI.md#ListGatewayServiceAccounts) | **Get** /api/hypershell/v1/gateways/{gateway_id}/service_accounts | List OpenShell gateway service accounts [**ListGateways**](DefaultAPI.md#ListGateways) | **Get** /api/hypershell/v1/gateways | Returns a list of gateways @@ -38,6 +42,7 @@ Method | HTTP request | Description [**RevokeGatewayServiceAccount**](DefaultAPI.md#RevokeGatewayServiceAccount) | **Post** /api/hypershell/v1/gateways/{gateway_id}/service_accounts/{service_account_id}/revoke | Permanently revoke an OpenShell gateway service account [**UpdateGateway**](DefaultAPI.md#UpdateGateway) | **Patch** /api/hypershell/v1/gateways/{id} | Update an gateway [**UpdateGatewayNetwork**](DefaultAPI.md#UpdateGatewayNetwork) | **Patch** /api/hypershell/v1/gateway_networks/{id} | Update an gatewayNetwork +[**UpdateGatewayProfile**](DefaultAPI.md#UpdateGatewayProfile) | **Patch** /api/hypershell/v1/gateway_profiles/{id} | Update a gatewayProfile [**UpdateGatewayRelease**](DefaultAPI.md#UpdateGatewayRelease) | **Patch** /api/hypershell/v1/gateway_releases/{id} | Update an gatewayRelease [**UpdateManagedCluster**](DefaultAPI.md#UpdateManagedCluster) | **Patch** /api/hypershell/v1/managed_clusters/{id} | Update an managedCluster [**UpdateManagedDatabase**](DefaultAPI.md#UpdateManagedDatabase) | **Patch** /api/hypershell/v1/managed_databases/{id} | Update an managedDatabase @@ -172,6 +177,70 @@ Name | Type | Description | Notes [[Back to README]](../README.md) +## CreateGatewayProfile + +> GatewayProfile CreateGatewayProfile(ctx).GatewayProfile(gatewayProfile).Execute() + +Create a new gatewayProfile + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID" +) + +func main() { + gatewayProfile := *openapiclient.NewGatewayProfile("Name_example") // GatewayProfile | GatewayProfile data + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.DefaultAPI.CreateGatewayProfile(context.Background()).GatewayProfile(gatewayProfile).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `DefaultAPI.CreateGatewayProfile``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `CreateGatewayProfile`: GatewayProfile + fmt.Fprintf(os.Stdout, "Response from `DefaultAPI.CreateGatewayProfile`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiCreateGatewayProfileRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **gatewayProfile** | [**GatewayProfile**](GatewayProfile.md) | GatewayProfile data | + +### Return type + +[**GatewayProfile**](GatewayProfile.md) + +### Authorization + +[Bearer](../README.md#Bearer) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## CreateGatewayRelease > GatewayRelease CreateGatewayRelease(ctx).GatewayRelease(gatewayRelease).Execute() @@ -630,6 +699,72 @@ Name | Type | Description | Notes [[Back to README]](../README.md) +## DeleteGatewayProfile + +> DeleteGatewayProfile(ctx, id).Execute() + +Delete a gateway profile + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID" +) + +func main() { + id := "id_example" // string | The id of record + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + r, err := apiClient.DefaultAPI.DeleteGatewayProfile(context.Background(), id).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `DefaultAPI.DeleteGatewayProfile``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } +} +``` + +### Path Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- +**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. +**id** | **string** | The id of record | + +### Other Parameters + +Other parameters are passed through a pointer to a apiDeleteGatewayProfileRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + + +### Return type + + (empty response body) + +### Authorization + +[Bearer](../README.md#Bearer) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## DeleteGatewayRelease > DeleteGatewayRelease(ctx, id).Execute() @@ -1101,6 +1236,74 @@ Name | Type | Description | Notes [[Back to README]](../README.md) +## GetGatewayProfile + +> GatewayProfile GetGatewayProfile(ctx, id).Execute() + +Get a gatewayProfile by id + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID" +) + +func main() { + id := "id_example" // string | The id of record + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.DefaultAPI.GetGatewayProfile(context.Background(), id).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `DefaultAPI.GetGatewayProfile``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `GetGatewayProfile`: GatewayProfile + fmt.Fprintf(os.Stdout, "Response from `DefaultAPI.GetGatewayProfile`: %v\n", resp) +} +``` + +### Path Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- +**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. +**id** | **string** | The id of record | + +### Other Parameters + +Other parameters are passed through a pointer to a apiGetGatewayProfileRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + + +### Return type + +[**GatewayProfile**](GatewayProfile.md) + +### Authorization + +[Bearer](../README.md#Bearer) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## GetGatewayRelease > GatewayRelease GetGatewayRelease(ctx, id).Execute() @@ -1643,6 +1846,78 @@ Name | Type | Description | Notes [[Back to README]](../README.md) +## ListGatewayProfiles + +> GatewayProfileList ListGatewayProfiles(ctx).Page(page).Size(size).Search(search).OrderBy(orderBy).Fields(fields).Execute() + +Returns a list of gatewayProfiles + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID" +) + +func main() { + page := int32(56) // int32 | Page number of record list when record list exceeds specified page size (optional) (default to 1) + size := int32(56) // int32 | Maximum number of records to return (optional) (default to 100) + search := "search_example" // string | Specifies the search criteria (optional) + orderBy := "orderBy_example" // string | Specifies the order by criteria (optional) + fields := "fields_example" // string | Supplies a comma-separated list of fields to be returned (optional) + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.DefaultAPI.ListGatewayProfiles(context.Background()).Page(page).Size(size).Search(search).OrderBy(orderBy).Fields(fields).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `DefaultAPI.ListGatewayProfiles``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `ListGatewayProfiles`: GatewayProfileList + fmt.Fprintf(os.Stdout, "Response from `DefaultAPI.ListGatewayProfiles`: %v\n", resp) +} +``` + +### Path Parameters + + + +### Other Parameters + +Other parameters are passed through a pointer to a apiListGatewayProfilesRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + **page** | **int32** | Page number of record list when record list exceeds specified page size | [default to 1] + **size** | **int32** | Maximum number of records to return | [default to 100] + **search** | **string** | Specifies the search criteria | + **orderBy** | **string** | Specifies the order by criteria | + **fields** | **string** | Supplies a comma-separated list of fields to be returned | + +### Return type + +[**GatewayProfileList**](GatewayProfileList.md) + +### Authorization + +[Bearer](../README.md#Bearer) + +### HTTP request headers + +- **Content-Type**: Not defined +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## ListGatewayReleases > GatewayReleaseList ListGatewayReleases(ctx).Page(page).Size(size).Search(search).OrderBy(orderBy).Fields(fields).Execute() @@ -2366,6 +2641,76 @@ Name | Type | Description | Notes [[Back to README]](../README.md) +## UpdateGatewayProfile + +> GatewayProfile UpdateGatewayProfile(ctx, id).GatewayProfilePatchRequest(gatewayProfilePatchRequest).Execute() + +Update a gatewayProfile + +### Example + +```go +package main + +import ( + "context" + "fmt" + "os" + openapiclient "github.com/GIT_USER_ID/GIT_REPO_ID" +) + +func main() { + id := "id_example" // string | The id of record + gatewayProfilePatchRequest := *openapiclient.NewGatewayProfilePatchRequest() // GatewayProfilePatchRequest | Updated gatewayProfile data + + configuration := openapiclient.NewConfiguration() + apiClient := openapiclient.NewAPIClient(configuration) + resp, r, err := apiClient.DefaultAPI.UpdateGatewayProfile(context.Background(), id).GatewayProfilePatchRequest(gatewayProfilePatchRequest).Execute() + if err != nil { + fmt.Fprintf(os.Stderr, "Error when calling `DefaultAPI.UpdateGatewayProfile``: %v\n", err) + fmt.Fprintf(os.Stderr, "Full HTTP response: %v\n", r) + } + // response from `UpdateGatewayProfile`: GatewayProfile + fmt.Fprintf(os.Stdout, "Response from `DefaultAPI.UpdateGatewayProfile`: %v\n", resp) +} +``` + +### Path Parameters + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- +**ctx** | **context.Context** | context for authentication, logging, cancellation, deadlines, tracing, etc. +**id** | **string** | The id of record | + +### Other Parameters + +Other parameters are passed through a pointer to a apiUpdateGatewayProfileRequest struct via the builder pattern + + +Name | Type | Description | Notes +------------- | ------------- | ------------- | ------------- + + **gatewayProfilePatchRequest** | [**GatewayProfilePatchRequest**](GatewayProfilePatchRequest.md) | Updated gatewayProfile data | + +### Return type + +[**GatewayProfile**](GatewayProfile.md) + +### Authorization + +[Bearer](../README.md#Bearer) + +### HTTP request headers + +- **Content-Type**: application/json +- **Accept**: application/json + +[[Back to top]](#) [[Back to API list]](../README.md#documentation-for-api-endpoints) +[[Back to Model list]](../README.md#documentation-for-models) +[[Back to README]](../README.md) + + ## UpdateGatewayRelease > GatewayRelease UpdateGatewayRelease(ctx, id).GatewayReleasePatchRequest(gatewayReleasePatchRequest).Execute() diff --git a/components/api-server/pkg/api/openapi/docs/Gateway.md b/components/api-server/pkg/api/openapi/docs/Gateway.md index ccbbd580..c06fc264 100644 --- a/components/api-server/pkg/api/openapi/docs/Gateway.md +++ b/components/api-server/pkg/api/openapi/docs/Gateway.md @@ -12,6 +12,7 @@ Name | Type | Description | Notes **Name** | **string** | | **ClusterId** | **string** | | **ReleaseId** | **string** | | +**ProfileId** | Pointer to **string** | GatewayProfile identifier used for namespace quota enforcement; required at creation (client-supplied or inherited from the cluster default) and reassignable but not clearable afterward | [optional] **DatabaseId** | **string** | Server-assigned ManagedDatabase identifier; client-supplied values are ignored | **Namespace** | **string** | API-assigned Kubernetes namespace derived from the Gateway identifier | [readonly] **ExternalDns** | Pointer to **string** | | [optional] @@ -234,6 +235,31 @@ and a boolean to check if the value has been set. SetReleaseId sets ReleaseId field to given value. +### GetProfileId + +`func (o *Gateway) GetProfileId() string` + +GetProfileId returns the ProfileId field if non-nil, zero value otherwise. + +### GetProfileIdOk + +`func (o *Gateway) GetProfileIdOk() (*string, bool)` + +GetProfileIdOk returns a tuple with the ProfileId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetProfileId + +`func (o *Gateway) SetProfileId(v string)` + +SetProfileId sets ProfileId field to given value. + +### HasProfileId + +`func (o *Gateway) HasProfileId() bool` + +HasProfileId returns a boolean if a field has been set. + ### GetDatabaseId `func (o *Gateway) GetDatabaseId() string` diff --git a/components/api-server/pkg/api/openapi/docs/GatewayCreateRequest.md b/components/api-server/pkg/api/openapi/docs/GatewayCreateRequest.md index b7479114..19d5e2f8 100644 --- a/components/api-server/pkg/api/openapi/docs/GatewayCreateRequest.md +++ b/components/api-server/pkg/api/openapi/docs/GatewayCreateRequest.md @@ -7,6 +7,7 @@ Name | Type | Description | Notes **Name** | **string** | | **ClusterId** | **string** | | **ReleaseId** | **string** | | +**ProfileId** | Pointer to **string** | Optional GatewayProfile identifier for quota enforcement; if omitted the API server falls back to the cluster's default profile, and rejects the request with HTTP 400 if neither is provided | [optional] **DatabaseId** | **string** | Required placement placeholder; the API server ignores its value and assigns the ManagedDatabase | **ExternalDns** | Pointer to **string** | | [optional] **TlsMode** | Pointer to **string** | | [optional] @@ -99,6 +100,31 @@ and a boolean to check if the value has been set. SetReleaseId sets ReleaseId field to given value. +### GetProfileId + +`func (o *GatewayCreateRequest) GetProfileId() string` + +GetProfileId returns the ProfileId field if non-nil, zero value otherwise. + +### GetProfileIdOk + +`func (o *GatewayCreateRequest) GetProfileIdOk() (*string, bool)` + +GetProfileIdOk returns a tuple with the ProfileId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetProfileId + +`func (o *GatewayCreateRequest) SetProfileId(v string)` + +SetProfileId sets ProfileId field to given value. + +### HasProfileId + +`func (o *GatewayCreateRequest) HasProfileId() bool` + +HasProfileId returns a boolean if a field has been set. + ### GetDatabaseId `func (o *GatewayCreateRequest) GetDatabaseId() string` diff --git a/components/api-server/pkg/api/openapi/docs/GatewayPatchRequest.md b/components/api-server/pkg/api/openapi/docs/GatewayPatchRequest.md index 2e6098eb..de437f2b 100644 --- a/components/api-server/pkg/api/openapi/docs/GatewayPatchRequest.md +++ b/components/api-server/pkg/api/openapi/docs/GatewayPatchRequest.md @@ -7,6 +7,7 @@ Name | Type | Description | Notes **Name** | Pointer to **string** | | [optional] **ClusterId** | Pointer to **string** | | [optional] **ReleaseId** | Pointer to **string** | | [optional] +**ProfileId** | Pointer to **string** | Reassign the gateway to a different GatewayProfile; cannot be set to null or empty (HTTP 400) and the target profile must exist | [optional] **DatabaseId** | Pointer to **string** | Server-owned placement field; values supplied through PATCH are ignored | [optional] **ExternalDns** | Pointer to **string** | | [optional] **TlsMode** | Pointer to **string** | | [optional] @@ -115,6 +116,31 @@ SetReleaseId sets ReleaseId field to given value. HasReleaseId returns a boolean if a field has been set. +### GetProfileId + +`func (o *GatewayPatchRequest) GetProfileId() string` + +GetProfileId returns the ProfileId field if non-nil, zero value otherwise. + +### GetProfileIdOk + +`func (o *GatewayPatchRequest) GetProfileIdOk() (*string, bool)` + +GetProfileIdOk returns a tuple with the ProfileId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetProfileId + +`func (o *GatewayPatchRequest) SetProfileId(v string)` + +SetProfileId sets ProfileId field to given value. + +### HasProfileId + +`func (o *GatewayPatchRequest) HasProfileId() bool` + +HasProfileId returns a boolean if a field has been set. + ### GetDatabaseId `func (o *GatewayPatchRequest) GetDatabaseId() string` diff --git a/components/api-server/pkg/api/openapi/docs/GatewayProfile.md b/components/api-server/pkg/api/openapi/docs/GatewayProfile.md new file mode 100644 index 00000000..879a2121 --- /dev/null +++ b/components/api-server/pkg/api/openapi/docs/GatewayProfile.md @@ -0,0 +1,493 @@ +# GatewayProfile + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Id** | Pointer to **string** | | [optional] +**Kind** | Pointer to **string** | | [optional] +**Href** | Pointer to **string** | | [optional] +**CreatedAt** | Pointer to **time.Time** | | [optional] +**UpdatedAt** | Pointer to **time.Time** | | [optional] +**Name** | **string** | | +**Description** | Pointer to **string** | Profile purpose/intent | [optional] +**CpuRequestTotal** | Pointer to **string** | Total CPU requests allowed (e.g., \"4\", \"500m\") | [optional] +**CpuLimitTotal** | Pointer to **string** | Total CPU limits allowed | [optional] +**MemoryRequestTotal** | Pointer to **string** | Total memory requests allowed (e.g., \"8Gi\", \"512Mi\") | [optional] +**MemoryLimitTotal** | Pointer to **string** | Total memory limits allowed | [optional] +**EphemeralStorageTotal** | Pointer to **string** | Total ephemeral storage allowed (e.g., \"10Gi\") | [optional] +**PodCount** | Pointer to **int32** | Maximum number of pods (0 means not set) | [optional] +**PvcCount** | Pointer to **int32** | Maximum number of PersistentVolumeClaims (0 means not set) | [optional] +**ContainerCpuRequestDefault** | Pointer to **string** | Default CPU request injected into containers without explicit request | [optional] +**ContainerCpuLimitMax** | Pointer to **string** | Maximum CPU limit a container can request | [optional] +**ContainerMemoryRequestDefault** | Pointer to **string** | Default memory request injected into containers | [optional] +**ContainerMemoryLimitMax** | Pointer to **string** | Maximum memory limit a container can request | [optional] + +## Methods + +### NewGatewayProfile + +`func NewGatewayProfile(name string, ) *GatewayProfile` + +NewGatewayProfile instantiates a new GatewayProfile object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewGatewayProfileWithDefaults + +`func NewGatewayProfileWithDefaults() *GatewayProfile` + +NewGatewayProfileWithDefaults instantiates a new GatewayProfile object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetId + +`func (o *GatewayProfile) GetId() string` + +GetId returns the Id field if non-nil, zero value otherwise. + +### GetIdOk + +`func (o *GatewayProfile) GetIdOk() (*string, bool)` + +GetIdOk returns a tuple with the Id field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetId + +`func (o *GatewayProfile) SetId(v string)` + +SetId sets Id field to given value. + +### HasId + +`func (o *GatewayProfile) HasId() bool` + +HasId returns a boolean if a field has been set. + +### GetKind + +`func (o *GatewayProfile) GetKind() string` + +GetKind returns the Kind field if non-nil, zero value otherwise. + +### GetKindOk + +`func (o *GatewayProfile) GetKindOk() (*string, bool)` + +GetKindOk returns a tuple with the Kind field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetKind + +`func (o *GatewayProfile) SetKind(v string)` + +SetKind sets Kind field to given value. + +### HasKind + +`func (o *GatewayProfile) HasKind() bool` + +HasKind returns a boolean if a field has been set. + +### GetHref + +`func (o *GatewayProfile) GetHref() string` + +GetHref returns the Href field if non-nil, zero value otherwise. + +### GetHrefOk + +`func (o *GatewayProfile) GetHrefOk() (*string, bool)` + +GetHrefOk returns a tuple with the Href field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetHref + +`func (o *GatewayProfile) SetHref(v string)` + +SetHref sets Href field to given value. + +### HasHref + +`func (o *GatewayProfile) HasHref() bool` + +HasHref returns a boolean if a field has been set. + +### GetCreatedAt + +`func (o *GatewayProfile) GetCreatedAt() time.Time` + +GetCreatedAt returns the CreatedAt field if non-nil, zero value otherwise. + +### GetCreatedAtOk + +`func (o *GatewayProfile) GetCreatedAtOk() (*time.Time, bool)` + +GetCreatedAtOk returns a tuple with the CreatedAt field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetCreatedAt + +`func (o *GatewayProfile) SetCreatedAt(v time.Time)` + +SetCreatedAt sets CreatedAt field to given value. + +### HasCreatedAt + +`func (o *GatewayProfile) HasCreatedAt() bool` + +HasCreatedAt returns a boolean if a field has been set. + +### GetUpdatedAt + +`func (o *GatewayProfile) GetUpdatedAt() time.Time` + +GetUpdatedAt returns the UpdatedAt field if non-nil, zero value otherwise. + +### GetUpdatedAtOk + +`func (o *GatewayProfile) GetUpdatedAtOk() (*time.Time, bool)` + +GetUpdatedAtOk returns a tuple with the UpdatedAt field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetUpdatedAt + +`func (o *GatewayProfile) SetUpdatedAt(v time.Time)` + +SetUpdatedAt sets UpdatedAt field to given value. + +### HasUpdatedAt + +`func (o *GatewayProfile) HasUpdatedAt() bool` + +HasUpdatedAt returns a boolean if a field has been set. + +### GetName + +`func (o *GatewayProfile) GetName() string` + +GetName returns the Name field if non-nil, zero value otherwise. + +### GetNameOk + +`func (o *GatewayProfile) GetNameOk() (*string, bool)` + +GetNameOk returns a tuple with the Name field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetName + +`func (o *GatewayProfile) SetName(v string)` + +SetName sets Name field to given value. + + +### GetDescription + +`func (o *GatewayProfile) GetDescription() string` + +GetDescription returns the Description field if non-nil, zero value otherwise. + +### GetDescriptionOk + +`func (o *GatewayProfile) GetDescriptionOk() (*string, bool)` + +GetDescriptionOk returns a tuple with the Description field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDescription + +`func (o *GatewayProfile) SetDescription(v string)` + +SetDescription sets Description field to given value. + +### HasDescription + +`func (o *GatewayProfile) HasDescription() bool` + +HasDescription returns a boolean if a field has been set. + +### GetCpuRequestTotal + +`func (o *GatewayProfile) GetCpuRequestTotal() string` + +GetCpuRequestTotal returns the CpuRequestTotal field if non-nil, zero value otherwise. + +### GetCpuRequestTotalOk + +`func (o *GatewayProfile) GetCpuRequestTotalOk() (*string, bool)` + +GetCpuRequestTotalOk returns a tuple with the CpuRequestTotal field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetCpuRequestTotal + +`func (o *GatewayProfile) SetCpuRequestTotal(v string)` + +SetCpuRequestTotal sets CpuRequestTotal field to given value. + +### HasCpuRequestTotal + +`func (o *GatewayProfile) HasCpuRequestTotal() bool` + +HasCpuRequestTotal returns a boolean if a field has been set. + +### GetCpuLimitTotal + +`func (o *GatewayProfile) GetCpuLimitTotal() string` + +GetCpuLimitTotal returns the CpuLimitTotal field if non-nil, zero value otherwise. + +### GetCpuLimitTotalOk + +`func (o *GatewayProfile) GetCpuLimitTotalOk() (*string, bool)` + +GetCpuLimitTotalOk returns a tuple with the CpuLimitTotal field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetCpuLimitTotal + +`func (o *GatewayProfile) SetCpuLimitTotal(v string)` + +SetCpuLimitTotal sets CpuLimitTotal field to given value. + +### HasCpuLimitTotal + +`func (o *GatewayProfile) HasCpuLimitTotal() bool` + +HasCpuLimitTotal returns a boolean if a field has been set. + +### GetMemoryRequestTotal + +`func (o *GatewayProfile) GetMemoryRequestTotal() string` + +GetMemoryRequestTotal returns the MemoryRequestTotal field if non-nil, zero value otherwise. + +### GetMemoryRequestTotalOk + +`func (o *GatewayProfile) GetMemoryRequestTotalOk() (*string, bool)` + +GetMemoryRequestTotalOk returns a tuple with the MemoryRequestTotal field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetMemoryRequestTotal + +`func (o *GatewayProfile) SetMemoryRequestTotal(v string)` + +SetMemoryRequestTotal sets MemoryRequestTotal field to given value. + +### HasMemoryRequestTotal + +`func (o *GatewayProfile) HasMemoryRequestTotal() bool` + +HasMemoryRequestTotal returns a boolean if a field has been set. + +### GetMemoryLimitTotal + +`func (o *GatewayProfile) GetMemoryLimitTotal() string` + +GetMemoryLimitTotal returns the MemoryLimitTotal field if non-nil, zero value otherwise. + +### GetMemoryLimitTotalOk + +`func (o *GatewayProfile) GetMemoryLimitTotalOk() (*string, bool)` + +GetMemoryLimitTotalOk returns a tuple with the MemoryLimitTotal field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetMemoryLimitTotal + +`func (o *GatewayProfile) SetMemoryLimitTotal(v string)` + +SetMemoryLimitTotal sets MemoryLimitTotal field to given value. + +### HasMemoryLimitTotal + +`func (o *GatewayProfile) HasMemoryLimitTotal() bool` + +HasMemoryLimitTotal returns a boolean if a field has been set. + +### GetEphemeralStorageTotal + +`func (o *GatewayProfile) GetEphemeralStorageTotal() string` + +GetEphemeralStorageTotal returns the EphemeralStorageTotal field if non-nil, zero value otherwise. + +### GetEphemeralStorageTotalOk + +`func (o *GatewayProfile) GetEphemeralStorageTotalOk() (*string, bool)` + +GetEphemeralStorageTotalOk returns a tuple with the EphemeralStorageTotal field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetEphemeralStorageTotal + +`func (o *GatewayProfile) SetEphemeralStorageTotal(v string)` + +SetEphemeralStorageTotal sets EphemeralStorageTotal field to given value. + +### HasEphemeralStorageTotal + +`func (o *GatewayProfile) HasEphemeralStorageTotal() bool` + +HasEphemeralStorageTotal returns a boolean if a field has been set. + +### GetPodCount + +`func (o *GatewayProfile) GetPodCount() int32` + +GetPodCount returns the PodCount field if non-nil, zero value otherwise. + +### GetPodCountOk + +`func (o *GatewayProfile) GetPodCountOk() (*int32, bool)` + +GetPodCountOk returns a tuple with the PodCount field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetPodCount + +`func (o *GatewayProfile) SetPodCount(v int32)` + +SetPodCount sets PodCount field to given value. + +### HasPodCount + +`func (o *GatewayProfile) HasPodCount() bool` + +HasPodCount returns a boolean if a field has been set. + +### GetPvcCount + +`func (o *GatewayProfile) GetPvcCount() int32` + +GetPvcCount returns the PvcCount field if non-nil, zero value otherwise. + +### GetPvcCountOk + +`func (o *GatewayProfile) GetPvcCountOk() (*int32, bool)` + +GetPvcCountOk returns a tuple with the PvcCount field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetPvcCount + +`func (o *GatewayProfile) SetPvcCount(v int32)` + +SetPvcCount sets PvcCount field to given value. + +### HasPvcCount + +`func (o *GatewayProfile) HasPvcCount() bool` + +HasPvcCount returns a boolean if a field has been set. + +### GetContainerCpuRequestDefault + +`func (o *GatewayProfile) GetContainerCpuRequestDefault() string` + +GetContainerCpuRequestDefault returns the ContainerCpuRequestDefault field if non-nil, zero value otherwise. + +### GetContainerCpuRequestDefaultOk + +`func (o *GatewayProfile) GetContainerCpuRequestDefaultOk() (*string, bool)` + +GetContainerCpuRequestDefaultOk returns a tuple with the ContainerCpuRequestDefault field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetContainerCpuRequestDefault + +`func (o *GatewayProfile) SetContainerCpuRequestDefault(v string)` + +SetContainerCpuRequestDefault sets ContainerCpuRequestDefault field to given value. + +### HasContainerCpuRequestDefault + +`func (o *GatewayProfile) HasContainerCpuRequestDefault() bool` + +HasContainerCpuRequestDefault returns a boolean if a field has been set. + +### GetContainerCpuLimitMax + +`func (o *GatewayProfile) GetContainerCpuLimitMax() string` + +GetContainerCpuLimitMax returns the ContainerCpuLimitMax field if non-nil, zero value otherwise. + +### GetContainerCpuLimitMaxOk + +`func (o *GatewayProfile) GetContainerCpuLimitMaxOk() (*string, bool)` + +GetContainerCpuLimitMaxOk returns a tuple with the ContainerCpuLimitMax field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetContainerCpuLimitMax + +`func (o *GatewayProfile) SetContainerCpuLimitMax(v string)` + +SetContainerCpuLimitMax sets ContainerCpuLimitMax field to given value. + +### HasContainerCpuLimitMax + +`func (o *GatewayProfile) HasContainerCpuLimitMax() bool` + +HasContainerCpuLimitMax returns a boolean if a field has been set. + +### GetContainerMemoryRequestDefault + +`func (o *GatewayProfile) GetContainerMemoryRequestDefault() string` + +GetContainerMemoryRequestDefault returns the ContainerMemoryRequestDefault field if non-nil, zero value otherwise. + +### GetContainerMemoryRequestDefaultOk + +`func (o *GatewayProfile) GetContainerMemoryRequestDefaultOk() (*string, bool)` + +GetContainerMemoryRequestDefaultOk returns a tuple with the ContainerMemoryRequestDefault field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetContainerMemoryRequestDefault + +`func (o *GatewayProfile) SetContainerMemoryRequestDefault(v string)` + +SetContainerMemoryRequestDefault sets ContainerMemoryRequestDefault field to given value. + +### HasContainerMemoryRequestDefault + +`func (o *GatewayProfile) HasContainerMemoryRequestDefault() bool` + +HasContainerMemoryRequestDefault returns a boolean if a field has been set. + +### GetContainerMemoryLimitMax + +`func (o *GatewayProfile) GetContainerMemoryLimitMax() string` + +GetContainerMemoryLimitMax returns the ContainerMemoryLimitMax field if non-nil, zero value otherwise. + +### GetContainerMemoryLimitMaxOk + +`func (o *GatewayProfile) GetContainerMemoryLimitMaxOk() (*string, bool)` + +GetContainerMemoryLimitMaxOk returns a tuple with the ContainerMemoryLimitMax field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetContainerMemoryLimitMax + +`func (o *GatewayProfile) SetContainerMemoryLimitMax(v string)` + +SetContainerMemoryLimitMax sets ContainerMemoryLimitMax field to given value. + +### HasContainerMemoryLimitMax + +`func (o *GatewayProfile) HasContainerMemoryLimitMax() bool` + +HasContainerMemoryLimitMax returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/components/api-server/pkg/api/openapi/docs/GatewayProfileList.md b/components/api-server/pkg/api/openapi/docs/GatewayProfileList.md new file mode 100644 index 00000000..2bf456bc --- /dev/null +++ b/components/api-server/pkg/api/openapi/docs/GatewayProfileList.md @@ -0,0 +1,264 @@ +# GatewayProfileList + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Kind** | Pointer to **string** | | [optional] +**Page** | Pointer to **int32** | | [optional] +**Size** | Pointer to **int32** | | [optional] +**Total** | Pointer to **int32** | | [optional] +**Id** | Pointer to **string** | | [optional] +**Href** | Pointer to **string** | | [optional] +**CreatedAt** | Pointer to **time.Time** | | [optional] +**UpdatedAt** | Pointer to **time.Time** | | [optional] +**Items** | Pointer to [**[]GatewayProfile**](GatewayProfile.md) | | [optional] + +## Methods + +### NewGatewayProfileList + +`func NewGatewayProfileList() *GatewayProfileList` + +NewGatewayProfileList instantiates a new GatewayProfileList object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewGatewayProfileListWithDefaults + +`func NewGatewayProfileListWithDefaults() *GatewayProfileList` + +NewGatewayProfileListWithDefaults instantiates a new GatewayProfileList object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetKind + +`func (o *GatewayProfileList) GetKind() string` + +GetKind returns the Kind field if non-nil, zero value otherwise. + +### GetKindOk + +`func (o *GatewayProfileList) GetKindOk() (*string, bool)` + +GetKindOk returns a tuple with the Kind field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetKind + +`func (o *GatewayProfileList) SetKind(v string)` + +SetKind sets Kind field to given value. + +### HasKind + +`func (o *GatewayProfileList) HasKind() bool` + +HasKind returns a boolean if a field has been set. + +### GetPage + +`func (o *GatewayProfileList) GetPage() int32` + +GetPage returns the Page field if non-nil, zero value otherwise. + +### GetPageOk + +`func (o *GatewayProfileList) GetPageOk() (*int32, bool)` + +GetPageOk returns a tuple with the Page field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetPage + +`func (o *GatewayProfileList) SetPage(v int32)` + +SetPage sets Page field to given value. + +### HasPage + +`func (o *GatewayProfileList) HasPage() bool` + +HasPage returns a boolean if a field has been set. + +### GetSize + +`func (o *GatewayProfileList) GetSize() int32` + +GetSize returns the Size field if non-nil, zero value otherwise. + +### GetSizeOk + +`func (o *GatewayProfileList) GetSizeOk() (*int32, bool)` + +GetSizeOk returns a tuple with the Size field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetSize + +`func (o *GatewayProfileList) SetSize(v int32)` + +SetSize sets Size field to given value. + +### HasSize + +`func (o *GatewayProfileList) HasSize() bool` + +HasSize returns a boolean if a field has been set. + +### GetTotal + +`func (o *GatewayProfileList) GetTotal() int32` + +GetTotal returns the Total field if non-nil, zero value otherwise. + +### GetTotalOk + +`func (o *GatewayProfileList) GetTotalOk() (*int32, bool)` + +GetTotalOk returns a tuple with the Total field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetTotal + +`func (o *GatewayProfileList) SetTotal(v int32)` + +SetTotal sets Total field to given value. + +### HasTotal + +`func (o *GatewayProfileList) HasTotal() bool` + +HasTotal returns a boolean if a field has been set. + +### GetId + +`func (o *GatewayProfileList) GetId() string` + +GetId returns the Id field if non-nil, zero value otherwise. + +### GetIdOk + +`func (o *GatewayProfileList) GetIdOk() (*string, bool)` + +GetIdOk returns a tuple with the Id field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetId + +`func (o *GatewayProfileList) SetId(v string)` + +SetId sets Id field to given value. + +### HasId + +`func (o *GatewayProfileList) HasId() bool` + +HasId returns a boolean if a field has been set. + +### GetHref + +`func (o *GatewayProfileList) GetHref() string` + +GetHref returns the Href field if non-nil, zero value otherwise. + +### GetHrefOk + +`func (o *GatewayProfileList) GetHrefOk() (*string, bool)` + +GetHrefOk returns a tuple with the Href field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetHref + +`func (o *GatewayProfileList) SetHref(v string)` + +SetHref sets Href field to given value. + +### HasHref + +`func (o *GatewayProfileList) HasHref() bool` + +HasHref returns a boolean if a field has been set. + +### GetCreatedAt + +`func (o *GatewayProfileList) GetCreatedAt() time.Time` + +GetCreatedAt returns the CreatedAt field if non-nil, zero value otherwise. + +### GetCreatedAtOk + +`func (o *GatewayProfileList) GetCreatedAtOk() (*time.Time, bool)` + +GetCreatedAtOk returns a tuple with the CreatedAt field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetCreatedAt + +`func (o *GatewayProfileList) SetCreatedAt(v time.Time)` + +SetCreatedAt sets CreatedAt field to given value. + +### HasCreatedAt + +`func (o *GatewayProfileList) HasCreatedAt() bool` + +HasCreatedAt returns a boolean if a field has been set. + +### GetUpdatedAt + +`func (o *GatewayProfileList) GetUpdatedAt() time.Time` + +GetUpdatedAt returns the UpdatedAt field if non-nil, zero value otherwise. + +### GetUpdatedAtOk + +`func (o *GatewayProfileList) GetUpdatedAtOk() (*time.Time, bool)` + +GetUpdatedAtOk returns a tuple with the UpdatedAt field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetUpdatedAt + +`func (o *GatewayProfileList) SetUpdatedAt(v time.Time)` + +SetUpdatedAt sets UpdatedAt field to given value. + +### HasUpdatedAt + +`func (o *GatewayProfileList) HasUpdatedAt() bool` + +HasUpdatedAt returns a boolean if a field has been set. + +### GetItems + +`func (o *GatewayProfileList) GetItems() []GatewayProfile` + +GetItems returns the Items field if non-nil, zero value otherwise. + +### GetItemsOk + +`func (o *GatewayProfileList) GetItemsOk() (*[]GatewayProfile, bool)` + +GetItemsOk returns a tuple with the Items field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetItems + +`func (o *GatewayProfileList) SetItems(v []GatewayProfile)` + +SetItems sets Items field to given value. + +### HasItems + +`func (o *GatewayProfileList) HasItems() bool` + +HasItems returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/components/api-server/pkg/api/openapi/docs/GatewayProfilePatchRequest.md b/components/api-server/pkg/api/openapi/docs/GatewayProfilePatchRequest.md new file mode 100644 index 00000000..ce1f6149 --- /dev/null +++ b/components/api-server/pkg/api/openapi/docs/GatewayProfilePatchRequest.md @@ -0,0 +1,368 @@ +# GatewayProfilePatchRequest + +## Properties + +Name | Type | Description | Notes +------------ | ------------- | ------------- | ------------- +**Name** | Pointer to **string** | | [optional] +**Description** | Pointer to **string** | | [optional] +**CpuRequestTotal** | Pointer to **string** | | [optional] +**CpuLimitTotal** | Pointer to **string** | | [optional] +**MemoryRequestTotal** | Pointer to **string** | | [optional] +**MemoryLimitTotal** | Pointer to **string** | | [optional] +**EphemeralStorageTotal** | Pointer to **string** | | [optional] +**PodCount** | Pointer to **int32** | | [optional] +**PvcCount** | Pointer to **int32** | | [optional] +**ContainerCpuRequestDefault** | Pointer to **string** | | [optional] +**ContainerCpuLimitMax** | Pointer to **string** | | [optional] +**ContainerMemoryRequestDefault** | Pointer to **string** | | [optional] +**ContainerMemoryLimitMax** | Pointer to **string** | | [optional] + +## Methods + +### NewGatewayProfilePatchRequest + +`func NewGatewayProfilePatchRequest() *GatewayProfilePatchRequest` + +NewGatewayProfilePatchRequest instantiates a new GatewayProfilePatchRequest object +This constructor will assign default values to properties that have it defined, +and makes sure properties required by API are set, but the set of arguments +will change when the set of required properties is changed + +### NewGatewayProfilePatchRequestWithDefaults + +`func NewGatewayProfilePatchRequestWithDefaults() *GatewayProfilePatchRequest` + +NewGatewayProfilePatchRequestWithDefaults instantiates a new GatewayProfilePatchRequest object +This constructor will only assign default values to properties that have it defined, +but it doesn't guarantee that properties required by API are set + +### GetName + +`func (o *GatewayProfilePatchRequest) GetName() string` + +GetName returns the Name field if non-nil, zero value otherwise. + +### GetNameOk + +`func (o *GatewayProfilePatchRequest) GetNameOk() (*string, bool)` + +GetNameOk returns a tuple with the Name field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetName + +`func (o *GatewayProfilePatchRequest) SetName(v string)` + +SetName sets Name field to given value. + +### HasName + +`func (o *GatewayProfilePatchRequest) HasName() bool` + +HasName returns a boolean if a field has been set. + +### GetDescription + +`func (o *GatewayProfilePatchRequest) GetDescription() string` + +GetDescription returns the Description field if non-nil, zero value otherwise. + +### GetDescriptionOk + +`func (o *GatewayProfilePatchRequest) GetDescriptionOk() (*string, bool)` + +GetDescriptionOk returns a tuple with the Description field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDescription + +`func (o *GatewayProfilePatchRequest) SetDescription(v string)` + +SetDescription sets Description field to given value. + +### HasDescription + +`func (o *GatewayProfilePatchRequest) HasDescription() bool` + +HasDescription returns a boolean if a field has been set. + +### GetCpuRequestTotal + +`func (o *GatewayProfilePatchRequest) GetCpuRequestTotal() string` + +GetCpuRequestTotal returns the CpuRequestTotal field if non-nil, zero value otherwise. + +### GetCpuRequestTotalOk + +`func (o *GatewayProfilePatchRequest) GetCpuRequestTotalOk() (*string, bool)` + +GetCpuRequestTotalOk returns a tuple with the CpuRequestTotal field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetCpuRequestTotal + +`func (o *GatewayProfilePatchRequest) SetCpuRequestTotal(v string)` + +SetCpuRequestTotal sets CpuRequestTotal field to given value. + +### HasCpuRequestTotal + +`func (o *GatewayProfilePatchRequest) HasCpuRequestTotal() bool` + +HasCpuRequestTotal returns a boolean if a field has been set. + +### GetCpuLimitTotal + +`func (o *GatewayProfilePatchRequest) GetCpuLimitTotal() string` + +GetCpuLimitTotal returns the CpuLimitTotal field if non-nil, zero value otherwise. + +### GetCpuLimitTotalOk + +`func (o *GatewayProfilePatchRequest) GetCpuLimitTotalOk() (*string, bool)` + +GetCpuLimitTotalOk returns a tuple with the CpuLimitTotal field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetCpuLimitTotal + +`func (o *GatewayProfilePatchRequest) SetCpuLimitTotal(v string)` + +SetCpuLimitTotal sets CpuLimitTotal field to given value. + +### HasCpuLimitTotal + +`func (o *GatewayProfilePatchRequest) HasCpuLimitTotal() bool` + +HasCpuLimitTotal returns a boolean if a field has been set. + +### GetMemoryRequestTotal + +`func (o *GatewayProfilePatchRequest) GetMemoryRequestTotal() string` + +GetMemoryRequestTotal returns the MemoryRequestTotal field if non-nil, zero value otherwise. + +### GetMemoryRequestTotalOk + +`func (o *GatewayProfilePatchRequest) GetMemoryRequestTotalOk() (*string, bool)` + +GetMemoryRequestTotalOk returns a tuple with the MemoryRequestTotal field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetMemoryRequestTotal + +`func (o *GatewayProfilePatchRequest) SetMemoryRequestTotal(v string)` + +SetMemoryRequestTotal sets MemoryRequestTotal field to given value. + +### HasMemoryRequestTotal + +`func (o *GatewayProfilePatchRequest) HasMemoryRequestTotal() bool` + +HasMemoryRequestTotal returns a boolean if a field has been set. + +### GetMemoryLimitTotal + +`func (o *GatewayProfilePatchRequest) GetMemoryLimitTotal() string` + +GetMemoryLimitTotal returns the MemoryLimitTotal field if non-nil, zero value otherwise. + +### GetMemoryLimitTotalOk + +`func (o *GatewayProfilePatchRequest) GetMemoryLimitTotalOk() (*string, bool)` + +GetMemoryLimitTotalOk returns a tuple with the MemoryLimitTotal field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetMemoryLimitTotal + +`func (o *GatewayProfilePatchRequest) SetMemoryLimitTotal(v string)` + +SetMemoryLimitTotal sets MemoryLimitTotal field to given value. + +### HasMemoryLimitTotal + +`func (o *GatewayProfilePatchRequest) HasMemoryLimitTotal() bool` + +HasMemoryLimitTotal returns a boolean if a field has been set. + +### GetEphemeralStorageTotal + +`func (o *GatewayProfilePatchRequest) GetEphemeralStorageTotal() string` + +GetEphemeralStorageTotal returns the EphemeralStorageTotal field if non-nil, zero value otherwise. + +### GetEphemeralStorageTotalOk + +`func (o *GatewayProfilePatchRequest) GetEphemeralStorageTotalOk() (*string, bool)` + +GetEphemeralStorageTotalOk returns a tuple with the EphemeralStorageTotal field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetEphemeralStorageTotal + +`func (o *GatewayProfilePatchRequest) SetEphemeralStorageTotal(v string)` + +SetEphemeralStorageTotal sets EphemeralStorageTotal field to given value. + +### HasEphemeralStorageTotal + +`func (o *GatewayProfilePatchRequest) HasEphemeralStorageTotal() bool` + +HasEphemeralStorageTotal returns a boolean if a field has been set. + +### GetPodCount + +`func (o *GatewayProfilePatchRequest) GetPodCount() int32` + +GetPodCount returns the PodCount field if non-nil, zero value otherwise. + +### GetPodCountOk + +`func (o *GatewayProfilePatchRequest) GetPodCountOk() (*int32, bool)` + +GetPodCountOk returns a tuple with the PodCount field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetPodCount + +`func (o *GatewayProfilePatchRequest) SetPodCount(v int32)` + +SetPodCount sets PodCount field to given value. + +### HasPodCount + +`func (o *GatewayProfilePatchRequest) HasPodCount() bool` + +HasPodCount returns a boolean if a field has been set. + +### GetPvcCount + +`func (o *GatewayProfilePatchRequest) GetPvcCount() int32` + +GetPvcCount returns the PvcCount field if non-nil, zero value otherwise. + +### GetPvcCountOk + +`func (o *GatewayProfilePatchRequest) GetPvcCountOk() (*int32, bool)` + +GetPvcCountOk returns a tuple with the PvcCount field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetPvcCount + +`func (o *GatewayProfilePatchRequest) SetPvcCount(v int32)` + +SetPvcCount sets PvcCount field to given value. + +### HasPvcCount + +`func (o *GatewayProfilePatchRequest) HasPvcCount() bool` + +HasPvcCount returns a boolean if a field has been set. + +### GetContainerCpuRequestDefault + +`func (o *GatewayProfilePatchRequest) GetContainerCpuRequestDefault() string` + +GetContainerCpuRequestDefault returns the ContainerCpuRequestDefault field if non-nil, zero value otherwise. + +### GetContainerCpuRequestDefaultOk + +`func (o *GatewayProfilePatchRequest) GetContainerCpuRequestDefaultOk() (*string, bool)` + +GetContainerCpuRequestDefaultOk returns a tuple with the ContainerCpuRequestDefault field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetContainerCpuRequestDefault + +`func (o *GatewayProfilePatchRequest) SetContainerCpuRequestDefault(v string)` + +SetContainerCpuRequestDefault sets ContainerCpuRequestDefault field to given value. + +### HasContainerCpuRequestDefault + +`func (o *GatewayProfilePatchRequest) HasContainerCpuRequestDefault() bool` + +HasContainerCpuRequestDefault returns a boolean if a field has been set. + +### GetContainerCpuLimitMax + +`func (o *GatewayProfilePatchRequest) GetContainerCpuLimitMax() string` + +GetContainerCpuLimitMax returns the ContainerCpuLimitMax field if non-nil, zero value otherwise. + +### GetContainerCpuLimitMaxOk + +`func (o *GatewayProfilePatchRequest) GetContainerCpuLimitMaxOk() (*string, bool)` + +GetContainerCpuLimitMaxOk returns a tuple with the ContainerCpuLimitMax field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetContainerCpuLimitMax + +`func (o *GatewayProfilePatchRequest) SetContainerCpuLimitMax(v string)` + +SetContainerCpuLimitMax sets ContainerCpuLimitMax field to given value. + +### HasContainerCpuLimitMax + +`func (o *GatewayProfilePatchRequest) HasContainerCpuLimitMax() bool` + +HasContainerCpuLimitMax returns a boolean if a field has been set. + +### GetContainerMemoryRequestDefault + +`func (o *GatewayProfilePatchRequest) GetContainerMemoryRequestDefault() string` + +GetContainerMemoryRequestDefault returns the ContainerMemoryRequestDefault field if non-nil, zero value otherwise. + +### GetContainerMemoryRequestDefaultOk + +`func (o *GatewayProfilePatchRequest) GetContainerMemoryRequestDefaultOk() (*string, bool)` + +GetContainerMemoryRequestDefaultOk returns a tuple with the ContainerMemoryRequestDefault field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetContainerMemoryRequestDefault + +`func (o *GatewayProfilePatchRequest) SetContainerMemoryRequestDefault(v string)` + +SetContainerMemoryRequestDefault sets ContainerMemoryRequestDefault field to given value. + +### HasContainerMemoryRequestDefault + +`func (o *GatewayProfilePatchRequest) HasContainerMemoryRequestDefault() bool` + +HasContainerMemoryRequestDefault returns a boolean if a field has been set. + +### GetContainerMemoryLimitMax + +`func (o *GatewayProfilePatchRequest) GetContainerMemoryLimitMax() string` + +GetContainerMemoryLimitMax returns the ContainerMemoryLimitMax field if non-nil, zero value otherwise. + +### GetContainerMemoryLimitMaxOk + +`func (o *GatewayProfilePatchRequest) GetContainerMemoryLimitMaxOk() (*string, bool)` + +GetContainerMemoryLimitMaxOk returns a tuple with the ContainerMemoryLimitMax field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetContainerMemoryLimitMax + +`func (o *GatewayProfilePatchRequest) SetContainerMemoryLimitMax(v string)` + +SetContainerMemoryLimitMax sets ContainerMemoryLimitMax field to given value. + +### HasContainerMemoryLimitMax + +`func (o *GatewayProfilePatchRequest) HasContainerMemoryLimitMax() bool` + +HasContainerMemoryLimitMax returns a boolean if a field has been set. + + +[[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) + + diff --git a/components/api-server/pkg/api/openapi/docs/ManagedCluster.md b/components/api-server/pkg/api/openapi/docs/ManagedCluster.md index 3b4ffcc1..ac1e8604 100644 --- a/components/api-server/pkg/api/openapi/docs/ManagedCluster.md +++ b/components/api-server/pkg/api/openapi/docs/ManagedCluster.md @@ -15,6 +15,8 @@ Name | Type | Description | Notes **KubeconfigSecret** | **string** | | **Status** | Pointer to **string** | | [optional] **ApiServerUrl** | Pointer to **string** | | [optional] +**ProfileId** | Pointer to **string** | Default GatewayProfile identifier inherited by gateways created on this cluster when they do not supply their own profile_id | [optional] +**DatabaseId** | Pointer to **string** | Default ManagedDatabase identifier for gateways placed on this cluster | [optional] ## Methods @@ -295,6 +297,56 @@ SetApiServerUrl sets ApiServerUrl field to given value. HasApiServerUrl returns a boolean if a field has been set. +### GetProfileId + +`func (o *ManagedCluster) GetProfileId() string` + +GetProfileId returns the ProfileId field if non-nil, zero value otherwise. + +### GetProfileIdOk + +`func (o *ManagedCluster) GetProfileIdOk() (*string, bool)` + +GetProfileIdOk returns a tuple with the ProfileId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetProfileId + +`func (o *ManagedCluster) SetProfileId(v string)` + +SetProfileId sets ProfileId field to given value. + +### HasProfileId + +`func (o *ManagedCluster) HasProfileId() bool` + +HasProfileId returns a boolean if a field has been set. + +### GetDatabaseId + +`func (o *ManagedCluster) GetDatabaseId() string` + +GetDatabaseId returns the DatabaseId field if non-nil, zero value otherwise. + +### GetDatabaseIdOk + +`func (o *ManagedCluster) GetDatabaseIdOk() (*string, bool)` + +GetDatabaseIdOk returns a tuple with the DatabaseId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDatabaseId + +`func (o *ManagedCluster) SetDatabaseId(v string)` + +SetDatabaseId sets DatabaseId field to given value. + +### HasDatabaseId + +`func (o *ManagedCluster) HasDatabaseId() bool` + +HasDatabaseId returns a boolean if a field has been set. + [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/components/api-server/pkg/api/openapi/docs/ManagedClusterPatchRequest.md b/components/api-server/pkg/api/openapi/docs/ManagedClusterPatchRequest.md index 5ee6d2d9..3818bd2e 100644 --- a/components/api-server/pkg/api/openapi/docs/ManagedClusterPatchRequest.md +++ b/components/api-server/pkg/api/openapi/docs/ManagedClusterPatchRequest.md @@ -10,6 +10,8 @@ Name | Type | Description | Notes **KubeconfigSecret** | Pointer to **string** | | [optional] **Status** | Pointer to **string** | | [optional] **ApiServerUrl** | Pointer to **string** | | [optional] +**ProfileId** | Pointer to **string** | Default GatewayProfile for gateways on this cluster; set to empty string to clear | [optional] +**DatabaseId** | Pointer to **string** | | [optional] ## Methods @@ -180,6 +182,56 @@ SetApiServerUrl sets ApiServerUrl field to given value. HasApiServerUrl returns a boolean if a field has been set. +### GetProfileId + +`func (o *ManagedClusterPatchRequest) GetProfileId() string` + +GetProfileId returns the ProfileId field if non-nil, zero value otherwise. + +### GetProfileIdOk + +`func (o *ManagedClusterPatchRequest) GetProfileIdOk() (*string, bool)` + +GetProfileIdOk returns a tuple with the ProfileId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetProfileId + +`func (o *ManagedClusterPatchRequest) SetProfileId(v string)` + +SetProfileId sets ProfileId field to given value. + +### HasProfileId + +`func (o *ManagedClusterPatchRequest) HasProfileId() bool` + +HasProfileId returns a boolean if a field has been set. + +### GetDatabaseId + +`func (o *ManagedClusterPatchRequest) GetDatabaseId() string` + +GetDatabaseId returns the DatabaseId field if non-nil, zero value otherwise. + +### GetDatabaseIdOk + +`func (o *ManagedClusterPatchRequest) GetDatabaseIdOk() (*string, bool)` + +GetDatabaseIdOk returns a tuple with the DatabaseId field if it's non-nil, zero value otherwise +and a boolean to check if the value has been set. + +### SetDatabaseId + +`func (o *ManagedClusterPatchRequest) SetDatabaseId(v string)` + +SetDatabaseId sets DatabaseId field to given value. + +### HasDatabaseId + +`func (o *ManagedClusterPatchRequest) HasDatabaseId() bool` + +HasDatabaseId returns a boolean if a field has been set. + [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/components/api-server/pkg/api/openapi/model_gateway.go b/components/api-server/pkg/api/openapi/model_gateway.go index 8f837743..fdebbebf 100644 --- a/components/api-server/pkg/api/openapi/model_gateway.go +++ b/components/api-server/pkg/api/openapi/model_gateway.go @@ -30,6 +30,8 @@ type Gateway struct { Name string `json:"name"` ClusterId string `json:"cluster_id"` ReleaseId string `json:"release_id"` + // GatewayProfile identifier used for namespace quota enforcement; required at creation (client-supplied or inherited from the cluster default) and reassignable but not clearable afterward + ProfileId *string `json:"profile_id,omitempty"` // Server-assigned ManagedDatabase identifier; client-supplied values are ignored DatabaseId string `json:"database_id"` // API-assigned Kubernetes namespace derived from the Gateway identifier @@ -317,6 +319,38 @@ func (o *Gateway) SetReleaseId(v string) { o.ReleaseId = v } +// GetProfileId returns the ProfileId field value if set, zero value otherwise. +func (o *Gateway) GetProfileId() string { + if o == nil || IsNil(o.ProfileId) { + var ret string + return ret + } + return *o.ProfileId +} + +// GetProfileIdOk returns a tuple with the ProfileId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *Gateway) GetProfileIdOk() (*string, bool) { + if o == nil || IsNil(o.ProfileId) { + return nil, false + } + return o.ProfileId, true +} + +// HasProfileId returns a boolean if a field has been set. +func (o *Gateway) HasProfileId() bool { + if o != nil && !IsNil(o.ProfileId) { + return true + } + + return false +} + +// SetProfileId gets a reference to the given string and assigns it to the ProfileId field. +func (o *Gateway) SetProfileId(v string) { + o.ProfileId = &v +} + // GetDatabaseId returns the DatabaseId field value func (o *Gateway) GetDatabaseId() string { if o == nil { @@ -873,6 +907,9 @@ func (o Gateway) ToMap() (map[string]interface{}, error) { toSerialize["name"] = o.Name toSerialize["cluster_id"] = o.ClusterId toSerialize["release_id"] = o.ReleaseId + if !IsNil(o.ProfileId) { + toSerialize["profile_id"] = o.ProfileId + } toSerialize["database_id"] = o.DatabaseId toSerialize["namespace"] = o.Namespace if !IsNil(o.ExternalDns) { diff --git a/components/api-server/pkg/api/openapi/model_gateway_create_request.go b/components/api-server/pkg/api/openapi/model_gateway_create_request.go index e6de84de..f5fb04a2 100644 --- a/components/api-server/pkg/api/openapi/model_gateway_create_request.go +++ b/components/api-server/pkg/api/openapi/model_gateway_create_request.go @@ -24,6 +24,8 @@ type GatewayCreateRequest struct { Name string `json:"name"` ClusterId string `json:"cluster_id"` ReleaseId string `json:"release_id"` + // Optional GatewayProfile identifier for quota enforcement; if omitted the API server falls back to the cluster's default profile, and rejects the request with HTTP 400 if neither is provided + ProfileId *string `json:"profile_id,omitempty"` // Required placement placeholder; the API server ignores its value and assigns the ManagedDatabase DatabaseId string `json:"database_id"` ExternalDns *string `json:"external_dns,omitempty"` @@ -140,6 +142,38 @@ func (o *GatewayCreateRequest) SetReleaseId(v string) { o.ReleaseId = v } +// GetProfileId returns the ProfileId field value if set, zero value otherwise. +func (o *GatewayCreateRequest) GetProfileId() string { + if o == nil || IsNil(o.ProfileId) { + var ret string + return ret + } + return *o.ProfileId +} + +// GetProfileIdOk returns a tuple with the ProfileId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayCreateRequest) GetProfileIdOk() (*string, bool) { + if o == nil || IsNil(o.ProfileId) { + return nil, false + } + return o.ProfileId, true +} + +// HasProfileId returns a boolean if a field has been set. +func (o *GatewayCreateRequest) HasProfileId() bool { + if o != nil && !IsNil(o.ProfileId) { + return true + } + + return false +} + +// SetProfileId gets a reference to the given string and assigns it to the ProfileId field. +func (o *GatewayCreateRequest) SetProfileId(v string) { + o.ProfileId = &v +} + // GetDatabaseId returns the DatabaseId field value func (o *GatewayCreateRequest) GetDatabaseId() string { if o == nil { @@ -529,6 +563,9 @@ func (o GatewayCreateRequest) ToMap() (map[string]interface{}, error) { toSerialize["name"] = o.Name toSerialize["cluster_id"] = o.ClusterId toSerialize["release_id"] = o.ReleaseId + if !IsNil(o.ProfileId) { + toSerialize["profile_id"] = o.ProfileId + } toSerialize["database_id"] = o.DatabaseId if !IsNil(o.ExternalDns) { toSerialize["external_dns"] = o.ExternalDns diff --git a/components/api-server/pkg/api/openapi/model_gateway_patch_request.go b/components/api-server/pkg/api/openapi/model_gateway_patch_request.go index fbfa0395..125d7949 100644 --- a/components/api-server/pkg/api/openapi/model_gateway_patch_request.go +++ b/components/api-server/pkg/api/openapi/model_gateway_patch_request.go @@ -22,6 +22,8 @@ type GatewayPatchRequest struct { Name *string `json:"name,omitempty"` ClusterId *string `json:"cluster_id,omitempty"` ReleaseId *string `json:"release_id,omitempty"` + // Reassign the gateway to a different GatewayProfile; cannot be set to null or empty (HTTP 400) and the target profile must exist + ProfileId *string `json:"profile_id,omitempty"` // Server-owned placement field; values supplied through PATCH are ignored DatabaseId *string `json:"database_id,omitempty"` ExternalDns *string `json:"external_dns,omitempty"` @@ -151,6 +153,38 @@ func (o *GatewayPatchRequest) SetReleaseId(v string) { o.ReleaseId = &v } +// GetProfileId returns the ProfileId field value if set, zero value otherwise. +func (o *GatewayPatchRequest) GetProfileId() string { + if o == nil || IsNil(o.ProfileId) { + var ret string + return ret + } + return *o.ProfileId +} + +// GetProfileIdOk returns a tuple with the ProfileId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayPatchRequest) GetProfileIdOk() (*string, bool) { + if o == nil || IsNil(o.ProfileId) { + return nil, false + } + return o.ProfileId, true +} + +// HasProfileId returns a boolean if a field has been set. +func (o *GatewayPatchRequest) HasProfileId() bool { + if o != nil && !IsNil(o.ProfileId) { + return true + } + + return false +} + +// SetProfileId gets a reference to the given string and assigns it to the ProfileId field. +func (o *GatewayPatchRequest) SetProfileId(v string) { + o.ProfileId = &v +} + // GetDatabaseId returns the DatabaseId field value if set, zero value otherwise. func (o *GatewayPatchRequest) GetDatabaseId() string { if o == nil || IsNil(o.DatabaseId) { @@ -586,6 +620,9 @@ func (o GatewayPatchRequest) ToMap() (map[string]interface{}, error) { if !IsNil(o.ReleaseId) { toSerialize["release_id"] = o.ReleaseId } + if !IsNil(o.ProfileId) { + toSerialize["profile_id"] = o.ProfileId + } if !IsNil(o.DatabaseId) { toSerialize["database_id"] = o.DatabaseId } diff --git a/components/api-server/pkg/api/openapi/model_gateway_profile.go b/components/api-server/pkg/api/openapi/model_gateway_profile.go new file mode 100644 index 00000000..fd294785 --- /dev/null +++ b/components/api-server/pkg/api/openapi/model_gateway_profile.go @@ -0,0 +1,781 @@ +/* +HyperShell API + +HyperShell gateway management API + +API version: 1.0.0 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "bytes" + "encoding/json" + "fmt" + "time" +) + +// checks if the GatewayProfile type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &GatewayProfile{} + +// GatewayProfile struct for GatewayProfile +type GatewayProfile struct { + Id *string `json:"id,omitempty"` + Kind *string `json:"kind,omitempty"` + Href *string `json:"href,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + Name string `json:"name"` + // Profile purpose/intent + Description *string `json:"description,omitempty"` + // Total CPU requests allowed (e.g., \"4\", \"500m\") + CpuRequestTotal *string `json:"cpu_request_total,omitempty"` + // Total CPU limits allowed + CpuLimitTotal *string `json:"cpu_limit_total,omitempty"` + // Total memory requests allowed (e.g., \"8Gi\", \"512Mi\") + MemoryRequestTotal *string `json:"memory_request_total,omitempty"` + // Total memory limits allowed + MemoryLimitTotal *string `json:"memory_limit_total,omitempty"` + // Total ephemeral storage allowed (e.g., \"10Gi\") + EphemeralStorageTotal *string `json:"ephemeral_storage_total,omitempty"` + // Maximum number of pods (0 means not set) + PodCount *int32 `json:"pod_count,omitempty"` + // Maximum number of PersistentVolumeClaims (0 means not set) + PvcCount *int32 `json:"pvc_count,omitempty"` + // Default CPU request injected into containers without explicit request + ContainerCpuRequestDefault *string `json:"container_cpu_request_default,omitempty"` + // Maximum CPU limit a container can request + ContainerCpuLimitMax *string `json:"container_cpu_limit_max,omitempty"` + // Default memory request injected into containers + ContainerMemoryRequestDefault *string `json:"container_memory_request_default,omitempty"` + // Maximum memory limit a container can request + ContainerMemoryLimitMax *string `json:"container_memory_limit_max,omitempty"` +} + +type _GatewayProfile GatewayProfile + +// NewGatewayProfile instantiates a new GatewayProfile object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewGatewayProfile(name string) *GatewayProfile { + this := GatewayProfile{} + this.Name = name + return &this +} + +// NewGatewayProfileWithDefaults instantiates a new GatewayProfile object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewGatewayProfileWithDefaults() *GatewayProfile { + this := GatewayProfile{} + return &this +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *GatewayProfile) GetId() string { + if o == nil || IsNil(o.Id) { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetIdOk() (*string, bool) { + if o == nil || IsNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *GatewayProfile) HasId() bool { + if o != nil && !IsNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *GatewayProfile) SetId(v string) { + o.Id = &v +} + +// GetKind returns the Kind field value if set, zero value otherwise. +func (o *GatewayProfile) GetKind() string { + if o == nil || IsNil(o.Kind) { + var ret string + return ret + } + return *o.Kind +} + +// GetKindOk returns a tuple with the Kind field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetKindOk() (*string, bool) { + if o == nil || IsNil(o.Kind) { + return nil, false + } + return o.Kind, true +} + +// HasKind returns a boolean if a field has been set. +func (o *GatewayProfile) HasKind() bool { + if o != nil && !IsNil(o.Kind) { + return true + } + + return false +} + +// SetKind gets a reference to the given string and assigns it to the Kind field. +func (o *GatewayProfile) SetKind(v string) { + o.Kind = &v +} + +// GetHref returns the Href field value if set, zero value otherwise. +func (o *GatewayProfile) GetHref() string { + if o == nil || IsNil(o.Href) { + var ret string + return ret + } + return *o.Href +} + +// GetHrefOk returns a tuple with the Href field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetHrefOk() (*string, bool) { + if o == nil || IsNil(o.Href) { + return nil, false + } + return o.Href, true +} + +// HasHref returns a boolean if a field has been set. +func (o *GatewayProfile) HasHref() bool { + if o != nil && !IsNil(o.Href) { + return true + } + + return false +} + +// SetHref gets a reference to the given string and assigns it to the Href field. +func (o *GatewayProfile) SetHref(v string) { + o.Href = &v +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *GatewayProfile) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *GatewayProfile) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *GatewayProfile) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *GatewayProfile) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *GatewayProfile) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *GatewayProfile) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetName returns the Name field value +func (o *GatewayProfile) GetName() string { + if o == nil { + var ret string + return ret + } + + return o.Name +} + +// GetNameOk returns a tuple with the Name field value +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetNameOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Name, true +} + +// SetName sets field value +func (o *GatewayProfile) SetName(v string) { + o.Name = v +} + +// GetDescription returns the Description field value if set, zero value otherwise. +func (o *GatewayProfile) GetDescription() string { + if o == nil || IsNil(o.Description) { + var ret string + return ret + } + return *o.Description +} + +// GetDescriptionOk returns a tuple with the Description field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetDescriptionOk() (*string, bool) { + if o == nil || IsNil(o.Description) { + return nil, false + } + return o.Description, true +} + +// HasDescription returns a boolean if a field has been set. +func (o *GatewayProfile) HasDescription() bool { + if o != nil && !IsNil(o.Description) { + return true + } + + return false +} + +// SetDescription gets a reference to the given string and assigns it to the Description field. +func (o *GatewayProfile) SetDescription(v string) { + o.Description = &v +} + +// GetCpuRequestTotal returns the CpuRequestTotal field value if set, zero value otherwise. +func (o *GatewayProfile) GetCpuRequestTotal() string { + if o == nil || IsNil(o.CpuRequestTotal) { + var ret string + return ret + } + return *o.CpuRequestTotal +} + +// GetCpuRequestTotalOk returns a tuple with the CpuRequestTotal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetCpuRequestTotalOk() (*string, bool) { + if o == nil || IsNil(o.CpuRequestTotal) { + return nil, false + } + return o.CpuRequestTotal, true +} + +// HasCpuRequestTotal returns a boolean if a field has been set. +func (o *GatewayProfile) HasCpuRequestTotal() bool { + if o != nil && !IsNil(o.CpuRequestTotal) { + return true + } + + return false +} + +// SetCpuRequestTotal gets a reference to the given string and assigns it to the CpuRequestTotal field. +func (o *GatewayProfile) SetCpuRequestTotal(v string) { + o.CpuRequestTotal = &v +} + +// GetCpuLimitTotal returns the CpuLimitTotal field value if set, zero value otherwise. +func (o *GatewayProfile) GetCpuLimitTotal() string { + if o == nil || IsNil(o.CpuLimitTotal) { + var ret string + return ret + } + return *o.CpuLimitTotal +} + +// GetCpuLimitTotalOk returns a tuple with the CpuLimitTotal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetCpuLimitTotalOk() (*string, bool) { + if o == nil || IsNil(o.CpuLimitTotal) { + return nil, false + } + return o.CpuLimitTotal, true +} + +// HasCpuLimitTotal returns a boolean if a field has been set. +func (o *GatewayProfile) HasCpuLimitTotal() bool { + if o != nil && !IsNil(o.CpuLimitTotal) { + return true + } + + return false +} + +// SetCpuLimitTotal gets a reference to the given string and assigns it to the CpuLimitTotal field. +func (o *GatewayProfile) SetCpuLimitTotal(v string) { + o.CpuLimitTotal = &v +} + +// GetMemoryRequestTotal returns the MemoryRequestTotal field value if set, zero value otherwise. +func (o *GatewayProfile) GetMemoryRequestTotal() string { + if o == nil || IsNil(o.MemoryRequestTotal) { + var ret string + return ret + } + return *o.MemoryRequestTotal +} + +// GetMemoryRequestTotalOk returns a tuple with the MemoryRequestTotal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetMemoryRequestTotalOk() (*string, bool) { + if o == nil || IsNil(o.MemoryRequestTotal) { + return nil, false + } + return o.MemoryRequestTotal, true +} + +// HasMemoryRequestTotal returns a boolean if a field has been set. +func (o *GatewayProfile) HasMemoryRequestTotal() bool { + if o != nil && !IsNil(o.MemoryRequestTotal) { + return true + } + + return false +} + +// SetMemoryRequestTotal gets a reference to the given string and assigns it to the MemoryRequestTotal field. +func (o *GatewayProfile) SetMemoryRequestTotal(v string) { + o.MemoryRequestTotal = &v +} + +// GetMemoryLimitTotal returns the MemoryLimitTotal field value if set, zero value otherwise. +func (o *GatewayProfile) GetMemoryLimitTotal() string { + if o == nil || IsNil(o.MemoryLimitTotal) { + var ret string + return ret + } + return *o.MemoryLimitTotal +} + +// GetMemoryLimitTotalOk returns a tuple with the MemoryLimitTotal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetMemoryLimitTotalOk() (*string, bool) { + if o == nil || IsNil(o.MemoryLimitTotal) { + return nil, false + } + return o.MemoryLimitTotal, true +} + +// HasMemoryLimitTotal returns a boolean if a field has been set. +func (o *GatewayProfile) HasMemoryLimitTotal() bool { + if o != nil && !IsNil(o.MemoryLimitTotal) { + return true + } + + return false +} + +// SetMemoryLimitTotal gets a reference to the given string and assigns it to the MemoryLimitTotal field. +func (o *GatewayProfile) SetMemoryLimitTotal(v string) { + o.MemoryLimitTotal = &v +} + +// GetEphemeralStorageTotal returns the EphemeralStorageTotal field value if set, zero value otherwise. +func (o *GatewayProfile) GetEphemeralStorageTotal() string { + if o == nil || IsNil(o.EphemeralStorageTotal) { + var ret string + return ret + } + return *o.EphemeralStorageTotal +} + +// GetEphemeralStorageTotalOk returns a tuple with the EphemeralStorageTotal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetEphemeralStorageTotalOk() (*string, bool) { + if o == nil || IsNil(o.EphemeralStorageTotal) { + return nil, false + } + return o.EphemeralStorageTotal, true +} + +// HasEphemeralStorageTotal returns a boolean if a field has been set. +func (o *GatewayProfile) HasEphemeralStorageTotal() bool { + if o != nil && !IsNil(o.EphemeralStorageTotal) { + return true + } + + return false +} + +// SetEphemeralStorageTotal gets a reference to the given string and assigns it to the EphemeralStorageTotal field. +func (o *GatewayProfile) SetEphemeralStorageTotal(v string) { + o.EphemeralStorageTotal = &v +} + +// GetPodCount returns the PodCount field value if set, zero value otherwise. +func (o *GatewayProfile) GetPodCount() int32 { + if o == nil || IsNil(o.PodCount) { + var ret int32 + return ret + } + return *o.PodCount +} + +// GetPodCountOk returns a tuple with the PodCount field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetPodCountOk() (*int32, bool) { + if o == nil || IsNil(o.PodCount) { + return nil, false + } + return o.PodCount, true +} + +// HasPodCount returns a boolean if a field has been set. +func (o *GatewayProfile) HasPodCount() bool { + if o != nil && !IsNil(o.PodCount) { + return true + } + + return false +} + +// SetPodCount gets a reference to the given int32 and assigns it to the PodCount field. +func (o *GatewayProfile) SetPodCount(v int32) { + o.PodCount = &v +} + +// GetPvcCount returns the PvcCount field value if set, zero value otherwise. +func (o *GatewayProfile) GetPvcCount() int32 { + if o == nil || IsNil(o.PvcCount) { + var ret int32 + return ret + } + return *o.PvcCount +} + +// GetPvcCountOk returns a tuple with the PvcCount field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetPvcCountOk() (*int32, bool) { + if o == nil || IsNil(o.PvcCount) { + return nil, false + } + return o.PvcCount, true +} + +// HasPvcCount returns a boolean if a field has been set. +func (o *GatewayProfile) HasPvcCount() bool { + if o != nil && !IsNil(o.PvcCount) { + return true + } + + return false +} + +// SetPvcCount gets a reference to the given int32 and assigns it to the PvcCount field. +func (o *GatewayProfile) SetPvcCount(v int32) { + o.PvcCount = &v +} + +// GetContainerCpuRequestDefault returns the ContainerCpuRequestDefault field value if set, zero value otherwise. +func (o *GatewayProfile) GetContainerCpuRequestDefault() string { + if o == nil || IsNil(o.ContainerCpuRequestDefault) { + var ret string + return ret + } + return *o.ContainerCpuRequestDefault +} + +// GetContainerCpuRequestDefaultOk returns a tuple with the ContainerCpuRequestDefault field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetContainerCpuRequestDefaultOk() (*string, bool) { + if o == nil || IsNil(o.ContainerCpuRequestDefault) { + return nil, false + } + return o.ContainerCpuRequestDefault, true +} + +// HasContainerCpuRequestDefault returns a boolean if a field has been set. +func (o *GatewayProfile) HasContainerCpuRequestDefault() bool { + if o != nil && !IsNil(o.ContainerCpuRequestDefault) { + return true + } + + return false +} + +// SetContainerCpuRequestDefault gets a reference to the given string and assigns it to the ContainerCpuRequestDefault field. +func (o *GatewayProfile) SetContainerCpuRequestDefault(v string) { + o.ContainerCpuRequestDefault = &v +} + +// GetContainerCpuLimitMax returns the ContainerCpuLimitMax field value if set, zero value otherwise. +func (o *GatewayProfile) GetContainerCpuLimitMax() string { + if o == nil || IsNil(o.ContainerCpuLimitMax) { + var ret string + return ret + } + return *o.ContainerCpuLimitMax +} + +// GetContainerCpuLimitMaxOk returns a tuple with the ContainerCpuLimitMax field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetContainerCpuLimitMaxOk() (*string, bool) { + if o == nil || IsNil(o.ContainerCpuLimitMax) { + return nil, false + } + return o.ContainerCpuLimitMax, true +} + +// HasContainerCpuLimitMax returns a boolean if a field has been set. +func (o *GatewayProfile) HasContainerCpuLimitMax() bool { + if o != nil && !IsNil(o.ContainerCpuLimitMax) { + return true + } + + return false +} + +// SetContainerCpuLimitMax gets a reference to the given string and assigns it to the ContainerCpuLimitMax field. +func (o *GatewayProfile) SetContainerCpuLimitMax(v string) { + o.ContainerCpuLimitMax = &v +} + +// GetContainerMemoryRequestDefault returns the ContainerMemoryRequestDefault field value if set, zero value otherwise. +func (o *GatewayProfile) GetContainerMemoryRequestDefault() string { + if o == nil || IsNil(o.ContainerMemoryRequestDefault) { + var ret string + return ret + } + return *o.ContainerMemoryRequestDefault +} + +// GetContainerMemoryRequestDefaultOk returns a tuple with the ContainerMemoryRequestDefault field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetContainerMemoryRequestDefaultOk() (*string, bool) { + if o == nil || IsNil(o.ContainerMemoryRequestDefault) { + return nil, false + } + return o.ContainerMemoryRequestDefault, true +} + +// HasContainerMemoryRequestDefault returns a boolean if a field has been set. +func (o *GatewayProfile) HasContainerMemoryRequestDefault() bool { + if o != nil && !IsNil(o.ContainerMemoryRequestDefault) { + return true + } + + return false +} + +// SetContainerMemoryRequestDefault gets a reference to the given string and assigns it to the ContainerMemoryRequestDefault field. +func (o *GatewayProfile) SetContainerMemoryRequestDefault(v string) { + o.ContainerMemoryRequestDefault = &v +} + +// GetContainerMemoryLimitMax returns the ContainerMemoryLimitMax field value if set, zero value otherwise. +func (o *GatewayProfile) GetContainerMemoryLimitMax() string { + if o == nil || IsNil(o.ContainerMemoryLimitMax) { + var ret string + return ret + } + return *o.ContainerMemoryLimitMax +} + +// GetContainerMemoryLimitMaxOk returns a tuple with the ContainerMemoryLimitMax field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfile) GetContainerMemoryLimitMaxOk() (*string, bool) { + if o == nil || IsNil(o.ContainerMemoryLimitMax) { + return nil, false + } + return o.ContainerMemoryLimitMax, true +} + +// HasContainerMemoryLimitMax returns a boolean if a field has been set. +func (o *GatewayProfile) HasContainerMemoryLimitMax() bool { + if o != nil && !IsNil(o.ContainerMemoryLimitMax) { + return true + } + + return false +} + +// SetContainerMemoryLimitMax gets a reference to the given string and assigns it to the ContainerMemoryLimitMax field. +func (o *GatewayProfile) SetContainerMemoryLimitMax(v string) { + o.ContainerMemoryLimitMax = &v +} + +func (o GatewayProfile) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o GatewayProfile) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + if !IsNil(o.Kind) { + toSerialize["kind"] = o.Kind + } + if !IsNil(o.Href) { + toSerialize["href"] = o.Href + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + toSerialize["name"] = o.Name + if !IsNil(o.Description) { + toSerialize["description"] = o.Description + } + if !IsNil(o.CpuRequestTotal) { + toSerialize["cpu_request_total"] = o.CpuRequestTotal + } + if !IsNil(o.CpuLimitTotal) { + toSerialize["cpu_limit_total"] = o.CpuLimitTotal + } + if !IsNil(o.MemoryRequestTotal) { + toSerialize["memory_request_total"] = o.MemoryRequestTotal + } + if !IsNil(o.MemoryLimitTotal) { + toSerialize["memory_limit_total"] = o.MemoryLimitTotal + } + if !IsNil(o.EphemeralStorageTotal) { + toSerialize["ephemeral_storage_total"] = o.EphemeralStorageTotal + } + if !IsNil(o.PodCount) { + toSerialize["pod_count"] = o.PodCount + } + if !IsNil(o.PvcCount) { + toSerialize["pvc_count"] = o.PvcCount + } + if !IsNil(o.ContainerCpuRequestDefault) { + toSerialize["container_cpu_request_default"] = o.ContainerCpuRequestDefault + } + if !IsNil(o.ContainerCpuLimitMax) { + toSerialize["container_cpu_limit_max"] = o.ContainerCpuLimitMax + } + if !IsNil(o.ContainerMemoryRequestDefault) { + toSerialize["container_memory_request_default"] = o.ContainerMemoryRequestDefault + } + if !IsNil(o.ContainerMemoryLimitMax) { + toSerialize["container_memory_limit_max"] = o.ContainerMemoryLimitMax + } + return toSerialize, nil +} + +func (o *GatewayProfile) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "name", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err + } + + for _, requiredProperty := range requiredProperties { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varGatewayProfile := _GatewayProfile{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varGatewayProfile) + + if err != nil { + return err + } + + *o = GatewayProfile(varGatewayProfile) + + return err +} + +type NullableGatewayProfile struct { + value *GatewayProfile + isSet bool +} + +func (v NullableGatewayProfile) Get() *GatewayProfile { + return v.value +} + +func (v *NullableGatewayProfile) Set(val *GatewayProfile) { + v.value = val + v.isSet = true +} + +func (v NullableGatewayProfile) IsSet() bool { + return v.isSet +} + +func (v *NullableGatewayProfile) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableGatewayProfile(val *GatewayProfile) *NullableGatewayProfile { + return &NullableGatewayProfile{value: val, isSet: true} +} + +func (v NullableGatewayProfile) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableGatewayProfile) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/components/api-server/pkg/api/openapi/model_gateway_profile_list.go b/components/api-server/pkg/api/openapi/model_gateway_profile_list.go new file mode 100644 index 00000000..68263742 --- /dev/null +++ b/components/api-server/pkg/api/openapi/model_gateway_profile_list.go @@ -0,0 +1,413 @@ +/* +HyperShell API + +HyperShell gateway management API + +API version: 1.0.0 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" + "time" +) + +// checks if the GatewayProfileList type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &GatewayProfileList{} + +// GatewayProfileList struct for GatewayProfileList +type GatewayProfileList struct { + Kind *string `json:"kind,omitempty"` + Page *int32 `json:"page,omitempty"` + Size *int32 `json:"size,omitempty"` + Total *int32 `json:"total,omitempty"` + Id *string `json:"id,omitempty"` + Href *string `json:"href,omitempty"` + CreatedAt *time.Time `json:"created_at,omitempty"` + UpdatedAt *time.Time `json:"updated_at,omitempty"` + Items []GatewayProfile `json:"items,omitempty"` +} + +// NewGatewayProfileList instantiates a new GatewayProfileList object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewGatewayProfileList() *GatewayProfileList { + this := GatewayProfileList{} + return &this +} + +// NewGatewayProfileListWithDefaults instantiates a new GatewayProfileList object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewGatewayProfileListWithDefaults() *GatewayProfileList { + this := GatewayProfileList{} + return &this +} + +// GetKind returns the Kind field value if set, zero value otherwise. +func (o *GatewayProfileList) GetKind() string { + if o == nil || IsNil(o.Kind) { + var ret string + return ret + } + return *o.Kind +} + +// GetKindOk returns a tuple with the Kind field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfileList) GetKindOk() (*string, bool) { + if o == nil || IsNil(o.Kind) { + return nil, false + } + return o.Kind, true +} + +// HasKind returns a boolean if a field has been set. +func (o *GatewayProfileList) HasKind() bool { + if o != nil && !IsNil(o.Kind) { + return true + } + + return false +} + +// SetKind gets a reference to the given string and assigns it to the Kind field. +func (o *GatewayProfileList) SetKind(v string) { + o.Kind = &v +} + +// GetPage returns the Page field value if set, zero value otherwise. +func (o *GatewayProfileList) GetPage() int32 { + if o == nil || IsNil(o.Page) { + var ret int32 + return ret + } + return *o.Page +} + +// GetPageOk returns a tuple with the Page field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfileList) GetPageOk() (*int32, bool) { + if o == nil || IsNil(o.Page) { + return nil, false + } + return o.Page, true +} + +// HasPage returns a boolean if a field has been set. +func (o *GatewayProfileList) HasPage() bool { + if o != nil && !IsNil(o.Page) { + return true + } + + return false +} + +// SetPage gets a reference to the given int32 and assigns it to the Page field. +func (o *GatewayProfileList) SetPage(v int32) { + o.Page = &v +} + +// GetSize returns the Size field value if set, zero value otherwise. +func (o *GatewayProfileList) GetSize() int32 { + if o == nil || IsNil(o.Size) { + var ret int32 + return ret + } + return *o.Size +} + +// GetSizeOk returns a tuple with the Size field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfileList) GetSizeOk() (*int32, bool) { + if o == nil || IsNil(o.Size) { + return nil, false + } + return o.Size, true +} + +// HasSize returns a boolean if a field has been set. +func (o *GatewayProfileList) HasSize() bool { + if o != nil && !IsNil(o.Size) { + return true + } + + return false +} + +// SetSize gets a reference to the given int32 and assigns it to the Size field. +func (o *GatewayProfileList) SetSize(v int32) { + o.Size = &v +} + +// GetTotal returns the Total field value if set, zero value otherwise. +func (o *GatewayProfileList) GetTotal() int32 { + if o == nil || IsNil(o.Total) { + var ret int32 + return ret + } + return *o.Total +} + +// GetTotalOk returns a tuple with the Total field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfileList) GetTotalOk() (*int32, bool) { + if o == nil || IsNil(o.Total) { + return nil, false + } + return o.Total, true +} + +// HasTotal returns a boolean if a field has been set. +func (o *GatewayProfileList) HasTotal() bool { + if o != nil && !IsNil(o.Total) { + return true + } + + return false +} + +// SetTotal gets a reference to the given int32 and assigns it to the Total field. +func (o *GatewayProfileList) SetTotal(v int32) { + o.Total = &v +} + +// GetId returns the Id field value if set, zero value otherwise. +func (o *GatewayProfileList) GetId() string { + if o == nil || IsNil(o.Id) { + var ret string + return ret + } + return *o.Id +} + +// GetIdOk returns a tuple with the Id field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfileList) GetIdOk() (*string, bool) { + if o == nil || IsNil(o.Id) { + return nil, false + } + return o.Id, true +} + +// HasId returns a boolean if a field has been set. +func (o *GatewayProfileList) HasId() bool { + if o != nil && !IsNil(o.Id) { + return true + } + + return false +} + +// SetId gets a reference to the given string and assigns it to the Id field. +func (o *GatewayProfileList) SetId(v string) { + o.Id = &v +} + +// GetHref returns the Href field value if set, zero value otherwise. +func (o *GatewayProfileList) GetHref() string { + if o == nil || IsNil(o.Href) { + var ret string + return ret + } + return *o.Href +} + +// GetHrefOk returns a tuple with the Href field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfileList) GetHrefOk() (*string, bool) { + if o == nil || IsNil(o.Href) { + return nil, false + } + return o.Href, true +} + +// HasHref returns a boolean if a field has been set. +func (o *GatewayProfileList) HasHref() bool { + if o != nil && !IsNil(o.Href) { + return true + } + + return false +} + +// SetHref gets a reference to the given string and assigns it to the Href field. +func (o *GatewayProfileList) SetHref(v string) { + o.Href = &v +} + +// GetCreatedAt returns the CreatedAt field value if set, zero value otherwise. +func (o *GatewayProfileList) GetCreatedAt() time.Time { + if o == nil || IsNil(o.CreatedAt) { + var ret time.Time + return ret + } + return *o.CreatedAt +} + +// GetCreatedAtOk returns a tuple with the CreatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfileList) GetCreatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.CreatedAt) { + return nil, false + } + return o.CreatedAt, true +} + +// HasCreatedAt returns a boolean if a field has been set. +func (o *GatewayProfileList) HasCreatedAt() bool { + if o != nil && !IsNil(o.CreatedAt) { + return true + } + + return false +} + +// SetCreatedAt gets a reference to the given time.Time and assigns it to the CreatedAt field. +func (o *GatewayProfileList) SetCreatedAt(v time.Time) { + o.CreatedAt = &v +} + +// GetUpdatedAt returns the UpdatedAt field value if set, zero value otherwise. +func (o *GatewayProfileList) GetUpdatedAt() time.Time { + if o == nil || IsNil(o.UpdatedAt) { + var ret time.Time + return ret + } + return *o.UpdatedAt +} + +// GetUpdatedAtOk returns a tuple with the UpdatedAt field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfileList) GetUpdatedAtOk() (*time.Time, bool) { + if o == nil || IsNil(o.UpdatedAt) { + return nil, false + } + return o.UpdatedAt, true +} + +// HasUpdatedAt returns a boolean if a field has been set. +func (o *GatewayProfileList) HasUpdatedAt() bool { + if o != nil && !IsNil(o.UpdatedAt) { + return true + } + + return false +} + +// SetUpdatedAt gets a reference to the given time.Time and assigns it to the UpdatedAt field. +func (o *GatewayProfileList) SetUpdatedAt(v time.Time) { + o.UpdatedAt = &v +} + +// GetItems returns the Items field value if set, zero value otherwise. +func (o *GatewayProfileList) GetItems() []GatewayProfile { + if o == nil || IsNil(o.Items) { + var ret []GatewayProfile + return ret + } + return o.Items +} + +// GetItemsOk returns a tuple with the Items field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfileList) GetItemsOk() ([]GatewayProfile, bool) { + if o == nil || IsNil(o.Items) { + return nil, false + } + return o.Items, true +} + +// HasItems returns a boolean if a field has been set. +func (o *GatewayProfileList) HasItems() bool { + if o != nil && !IsNil(o.Items) { + return true + } + + return false +} + +// SetItems gets a reference to the given []GatewayProfile and assigns it to the Items field. +func (o *GatewayProfileList) SetItems(v []GatewayProfile) { + o.Items = v +} + +func (o GatewayProfileList) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o GatewayProfileList) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Kind) { + toSerialize["kind"] = o.Kind + } + if !IsNil(o.Page) { + toSerialize["page"] = o.Page + } + if !IsNil(o.Size) { + toSerialize["size"] = o.Size + } + if !IsNil(o.Total) { + toSerialize["total"] = o.Total + } + if !IsNil(o.Id) { + toSerialize["id"] = o.Id + } + if !IsNil(o.Href) { + toSerialize["href"] = o.Href + } + if !IsNil(o.CreatedAt) { + toSerialize["created_at"] = o.CreatedAt + } + if !IsNil(o.UpdatedAt) { + toSerialize["updated_at"] = o.UpdatedAt + } + if !IsNil(o.Items) { + toSerialize["items"] = o.Items + } + return toSerialize, nil +} + +type NullableGatewayProfileList struct { + value *GatewayProfileList + isSet bool +} + +func (v NullableGatewayProfileList) Get() *GatewayProfileList { + return v.value +} + +func (v *NullableGatewayProfileList) Set(val *GatewayProfileList) { + v.value = val + v.isSet = true +} + +func (v NullableGatewayProfileList) IsSet() bool { + return v.isSet +} + +func (v *NullableGatewayProfileList) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableGatewayProfileList(val *GatewayProfileList) *NullableGatewayProfileList { + return &NullableGatewayProfileList{value: val, isSet: true} +} + +func (v NullableGatewayProfileList) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableGatewayProfileList) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/components/api-server/pkg/api/openapi/model_gateway_profile_patch_request.go b/components/api-server/pkg/api/openapi/model_gateway_profile_patch_request.go new file mode 100644 index 00000000..209f533a --- /dev/null +++ b/components/api-server/pkg/api/openapi/model_gateway_profile_patch_request.go @@ -0,0 +1,556 @@ +/* +HyperShell API + +HyperShell gateway management API + +API version: 1.0.0 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package openapi + +import ( + "encoding/json" +) + +// checks if the GatewayProfilePatchRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &GatewayProfilePatchRequest{} + +// GatewayProfilePatchRequest struct for GatewayProfilePatchRequest +type GatewayProfilePatchRequest struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + CpuRequestTotal *string `json:"cpu_request_total,omitempty"` + CpuLimitTotal *string `json:"cpu_limit_total,omitempty"` + MemoryRequestTotal *string `json:"memory_request_total,omitempty"` + MemoryLimitTotal *string `json:"memory_limit_total,omitempty"` + EphemeralStorageTotal *string `json:"ephemeral_storage_total,omitempty"` + PodCount *int32 `json:"pod_count,omitempty"` + PvcCount *int32 `json:"pvc_count,omitempty"` + ContainerCpuRequestDefault *string `json:"container_cpu_request_default,omitempty"` + ContainerCpuLimitMax *string `json:"container_cpu_limit_max,omitempty"` + ContainerMemoryRequestDefault *string `json:"container_memory_request_default,omitempty"` + ContainerMemoryLimitMax *string `json:"container_memory_limit_max,omitempty"` +} + +// NewGatewayProfilePatchRequest instantiates a new GatewayProfilePatchRequest object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewGatewayProfilePatchRequest() *GatewayProfilePatchRequest { + this := GatewayProfilePatchRequest{} + return &this +} + +// NewGatewayProfilePatchRequestWithDefaults instantiates a new GatewayProfilePatchRequest object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewGatewayProfilePatchRequestWithDefaults() *GatewayProfilePatchRequest { + this := GatewayProfilePatchRequest{} + return &this +} + +// GetName returns the Name field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetName() string { + if o == nil || IsNil(o.Name) { + var ret string + return ret + } + return *o.Name +} + +// GetNameOk returns a tuple with the Name field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetNameOk() (*string, bool) { + if o == nil || IsNil(o.Name) { + return nil, false + } + return o.Name, true +} + +// HasName returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasName() bool { + if o != nil && !IsNil(o.Name) { + return true + } + + return false +} + +// SetName gets a reference to the given string and assigns it to the Name field. +func (o *GatewayProfilePatchRequest) SetName(v string) { + o.Name = &v +} + +// GetDescription returns the Description field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetDescription() string { + if o == nil || IsNil(o.Description) { + var ret string + return ret + } + return *o.Description +} + +// GetDescriptionOk returns a tuple with the Description field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetDescriptionOk() (*string, bool) { + if o == nil || IsNil(o.Description) { + return nil, false + } + return o.Description, true +} + +// HasDescription returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasDescription() bool { + if o != nil && !IsNil(o.Description) { + return true + } + + return false +} + +// SetDescription gets a reference to the given string and assigns it to the Description field. +func (o *GatewayProfilePatchRequest) SetDescription(v string) { + o.Description = &v +} + +// GetCpuRequestTotal returns the CpuRequestTotal field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetCpuRequestTotal() string { + if o == nil || IsNil(o.CpuRequestTotal) { + var ret string + return ret + } + return *o.CpuRequestTotal +} + +// GetCpuRequestTotalOk returns a tuple with the CpuRequestTotal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetCpuRequestTotalOk() (*string, bool) { + if o == nil || IsNil(o.CpuRequestTotal) { + return nil, false + } + return o.CpuRequestTotal, true +} + +// HasCpuRequestTotal returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasCpuRequestTotal() bool { + if o != nil && !IsNil(o.CpuRequestTotal) { + return true + } + + return false +} + +// SetCpuRequestTotal gets a reference to the given string and assigns it to the CpuRequestTotal field. +func (o *GatewayProfilePatchRequest) SetCpuRequestTotal(v string) { + o.CpuRequestTotal = &v +} + +// GetCpuLimitTotal returns the CpuLimitTotal field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetCpuLimitTotal() string { + if o == nil || IsNil(o.CpuLimitTotal) { + var ret string + return ret + } + return *o.CpuLimitTotal +} + +// GetCpuLimitTotalOk returns a tuple with the CpuLimitTotal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetCpuLimitTotalOk() (*string, bool) { + if o == nil || IsNil(o.CpuLimitTotal) { + return nil, false + } + return o.CpuLimitTotal, true +} + +// HasCpuLimitTotal returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasCpuLimitTotal() bool { + if o != nil && !IsNil(o.CpuLimitTotal) { + return true + } + + return false +} + +// SetCpuLimitTotal gets a reference to the given string and assigns it to the CpuLimitTotal field. +func (o *GatewayProfilePatchRequest) SetCpuLimitTotal(v string) { + o.CpuLimitTotal = &v +} + +// GetMemoryRequestTotal returns the MemoryRequestTotal field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetMemoryRequestTotal() string { + if o == nil || IsNil(o.MemoryRequestTotal) { + var ret string + return ret + } + return *o.MemoryRequestTotal +} + +// GetMemoryRequestTotalOk returns a tuple with the MemoryRequestTotal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetMemoryRequestTotalOk() (*string, bool) { + if o == nil || IsNil(o.MemoryRequestTotal) { + return nil, false + } + return o.MemoryRequestTotal, true +} + +// HasMemoryRequestTotal returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasMemoryRequestTotal() bool { + if o != nil && !IsNil(o.MemoryRequestTotal) { + return true + } + + return false +} + +// SetMemoryRequestTotal gets a reference to the given string and assigns it to the MemoryRequestTotal field. +func (o *GatewayProfilePatchRequest) SetMemoryRequestTotal(v string) { + o.MemoryRequestTotal = &v +} + +// GetMemoryLimitTotal returns the MemoryLimitTotal field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetMemoryLimitTotal() string { + if o == nil || IsNil(o.MemoryLimitTotal) { + var ret string + return ret + } + return *o.MemoryLimitTotal +} + +// GetMemoryLimitTotalOk returns a tuple with the MemoryLimitTotal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetMemoryLimitTotalOk() (*string, bool) { + if o == nil || IsNil(o.MemoryLimitTotal) { + return nil, false + } + return o.MemoryLimitTotal, true +} + +// HasMemoryLimitTotal returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasMemoryLimitTotal() bool { + if o != nil && !IsNil(o.MemoryLimitTotal) { + return true + } + + return false +} + +// SetMemoryLimitTotal gets a reference to the given string and assigns it to the MemoryLimitTotal field. +func (o *GatewayProfilePatchRequest) SetMemoryLimitTotal(v string) { + o.MemoryLimitTotal = &v +} + +// GetEphemeralStorageTotal returns the EphemeralStorageTotal field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetEphemeralStorageTotal() string { + if o == nil || IsNil(o.EphemeralStorageTotal) { + var ret string + return ret + } + return *o.EphemeralStorageTotal +} + +// GetEphemeralStorageTotalOk returns a tuple with the EphemeralStorageTotal field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetEphemeralStorageTotalOk() (*string, bool) { + if o == nil || IsNil(o.EphemeralStorageTotal) { + return nil, false + } + return o.EphemeralStorageTotal, true +} + +// HasEphemeralStorageTotal returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasEphemeralStorageTotal() bool { + if o != nil && !IsNil(o.EphemeralStorageTotal) { + return true + } + + return false +} + +// SetEphemeralStorageTotal gets a reference to the given string and assigns it to the EphemeralStorageTotal field. +func (o *GatewayProfilePatchRequest) SetEphemeralStorageTotal(v string) { + o.EphemeralStorageTotal = &v +} + +// GetPodCount returns the PodCount field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetPodCount() int32 { + if o == nil || IsNil(o.PodCount) { + var ret int32 + return ret + } + return *o.PodCount +} + +// GetPodCountOk returns a tuple with the PodCount field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetPodCountOk() (*int32, bool) { + if o == nil || IsNil(o.PodCount) { + return nil, false + } + return o.PodCount, true +} + +// HasPodCount returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasPodCount() bool { + if o != nil && !IsNil(o.PodCount) { + return true + } + + return false +} + +// SetPodCount gets a reference to the given int32 and assigns it to the PodCount field. +func (o *GatewayProfilePatchRequest) SetPodCount(v int32) { + o.PodCount = &v +} + +// GetPvcCount returns the PvcCount field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetPvcCount() int32 { + if o == nil || IsNil(o.PvcCount) { + var ret int32 + return ret + } + return *o.PvcCount +} + +// GetPvcCountOk returns a tuple with the PvcCount field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetPvcCountOk() (*int32, bool) { + if o == nil || IsNil(o.PvcCount) { + return nil, false + } + return o.PvcCount, true +} + +// HasPvcCount returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasPvcCount() bool { + if o != nil && !IsNil(o.PvcCount) { + return true + } + + return false +} + +// SetPvcCount gets a reference to the given int32 and assigns it to the PvcCount field. +func (o *GatewayProfilePatchRequest) SetPvcCount(v int32) { + o.PvcCount = &v +} + +// GetContainerCpuRequestDefault returns the ContainerCpuRequestDefault field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetContainerCpuRequestDefault() string { + if o == nil || IsNil(o.ContainerCpuRequestDefault) { + var ret string + return ret + } + return *o.ContainerCpuRequestDefault +} + +// GetContainerCpuRequestDefaultOk returns a tuple with the ContainerCpuRequestDefault field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetContainerCpuRequestDefaultOk() (*string, bool) { + if o == nil || IsNil(o.ContainerCpuRequestDefault) { + return nil, false + } + return o.ContainerCpuRequestDefault, true +} + +// HasContainerCpuRequestDefault returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasContainerCpuRequestDefault() bool { + if o != nil && !IsNil(o.ContainerCpuRequestDefault) { + return true + } + + return false +} + +// SetContainerCpuRequestDefault gets a reference to the given string and assigns it to the ContainerCpuRequestDefault field. +func (o *GatewayProfilePatchRequest) SetContainerCpuRequestDefault(v string) { + o.ContainerCpuRequestDefault = &v +} + +// GetContainerCpuLimitMax returns the ContainerCpuLimitMax field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetContainerCpuLimitMax() string { + if o == nil || IsNil(o.ContainerCpuLimitMax) { + var ret string + return ret + } + return *o.ContainerCpuLimitMax +} + +// GetContainerCpuLimitMaxOk returns a tuple with the ContainerCpuLimitMax field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetContainerCpuLimitMaxOk() (*string, bool) { + if o == nil || IsNil(o.ContainerCpuLimitMax) { + return nil, false + } + return o.ContainerCpuLimitMax, true +} + +// HasContainerCpuLimitMax returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasContainerCpuLimitMax() bool { + if o != nil && !IsNil(o.ContainerCpuLimitMax) { + return true + } + + return false +} + +// SetContainerCpuLimitMax gets a reference to the given string and assigns it to the ContainerCpuLimitMax field. +func (o *GatewayProfilePatchRequest) SetContainerCpuLimitMax(v string) { + o.ContainerCpuLimitMax = &v +} + +// GetContainerMemoryRequestDefault returns the ContainerMemoryRequestDefault field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetContainerMemoryRequestDefault() string { + if o == nil || IsNil(o.ContainerMemoryRequestDefault) { + var ret string + return ret + } + return *o.ContainerMemoryRequestDefault +} + +// GetContainerMemoryRequestDefaultOk returns a tuple with the ContainerMemoryRequestDefault field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetContainerMemoryRequestDefaultOk() (*string, bool) { + if o == nil || IsNil(o.ContainerMemoryRequestDefault) { + return nil, false + } + return o.ContainerMemoryRequestDefault, true +} + +// HasContainerMemoryRequestDefault returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasContainerMemoryRequestDefault() bool { + if o != nil && !IsNil(o.ContainerMemoryRequestDefault) { + return true + } + + return false +} + +// SetContainerMemoryRequestDefault gets a reference to the given string and assigns it to the ContainerMemoryRequestDefault field. +func (o *GatewayProfilePatchRequest) SetContainerMemoryRequestDefault(v string) { + o.ContainerMemoryRequestDefault = &v +} + +// GetContainerMemoryLimitMax returns the ContainerMemoryLimitMax field value if set, zero value otherwise. +func (o *GatewayProfilePatchRequest) GetContainerMemoryLimitMax() string { + if o == nil || IsNil(o.ContainerMemoryLimitMax) { + var ret string + return ret + } + return *o.ContainerMemoryLimitMax +} + +// GetContainerMemoryLimitMaxOk returns a tuple with the ContainerMemoryLimitMax field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *GatewayProfilePatchRequest) GetContainerMemoryLimitMaxOk() (*string, bool) { + if o == nil || IsNil(o.ContainerMemoryLimitMax) { + return nil, false + } + return o.ContainerMemoryLimitMax, true +} + +// HasContainerMemoryLimitMax returns a boolean if a field has been set. +func (o *GatewayProfilePatchRequest) HasContainerMemoryLimitMax() bool { + if o != nil && !IsNil(o.ContainerMemoryLimitMax) { + return true + } + + return false +} + +// SetContainerMemoryLimitMax gets a reference to the given string and assigns it to the ContainerMemoryLimitMax field. +func (o *GatewayProfilePatchRequest) SetContainerMemoryLimitMax(v string) { + o.ContainerMemoryLimitMax = &v +} + +func (o GatewayProfilePatchRequest) MarshalJSON() ([]byte, error) { + toSerialize, err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o GatewayProfilePatchRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Name) { + toSerialize["name"] = o.Name + } + if !IsNil(o.Description) { + toSerialize["description"] = o.Description + } + if !IsNil(o.CpuRequestTotal) { + toSerialize["cpu_request_total"] = o.CpuRequestTotal + } + if !IsNil(o.CpuLimitTotal) { + toSerialize["cpu_limit_total"] = o.CpuLimitTotal + } + if !IsNil(o.MemoryRequestTotal) { + toSerialize["memory_request_total"] = o.MemoryRequestTotal + } + if !IsNil(o.MemoryLimitTotal) { + toSerialize["memory_limit_total"] = o.MemoryLimitTotal + } + if !IsNil(o.EphemeralStorageTotal) { + toSerialize["ephemeral_storage_total"] = o.EphemeralStorageTotal + } + if !IsNil(o.PodCount) { + toSerialize["pod_count"] = o.PodCount + } + if !IsNil(o.PvcCount) { + toSerialize["pvc_count"] = o.PvcCount + } + if !IsNil(o.ContainerCpuRequestDefault) { + toSerialize["container_cpu_request_default"] = o.ContainerCpuRequestDefault + } + if !IsNil(o.ContainerCpuLimitMax) { + toSerialize["container_cpu_limit_max"] = o.ContainerCpuLimitMax + } + if !IsNil(o.ContainerMemoryRequestDefault) { + toSerialize["container_memory_request_default"] = o.ContainerMemoryRequestDefault + } + if !IsNil(o.ContainerMemoryLimitMax) { + toSerialize["container_memory_limit_max"] = o.ContainerMemoryLimitMax + } + return toSerialize, nil +} + +type NullableGatewayProfilePatchRequest struct { + value *GatewayProfilePatchRequest + isSet bool +} + +func (v NullableGatewayProfilePatchRequest) Get() *GatewayProfilePatchRequest { + return v.value +} + +func (v *NullableGatewayProfilePatchRequest) Set(val *GatewayProfilePatchRequest) { + v.value = val + v.isSet = true +} + +func (v NullableGatewayProfilePatchRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableGatewayProfilePatchRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableGatewayProfilePatchRequest(val *GatewayProfilePatchRequest) *NullableGatewayProfilePatchRequest { + return &NullableGatewayProfilePatchRequest{value: val, isSet: true} +} + +func (v NullableGatewayProfilePatchRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableGatewayProfilePatchRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} diff --git a/components/api-server/pkg/api/openapi/model_managed_cluster.go b/components/api-server/pkg/api/openapi/model_managed_cluster.go index 83a7d2a3..69b19683 100644 --- a/components/api-server/pkg/api/openapi/model_managed_cluster.go +++ b/components/api-server/pkg/api/openapi/model_managed_cluster.go @@ -33,6 +33,10 @@ type ManagedCluster struct { KubeconfigSecret string `json:"kubeconfig_secret"` Status *string `json:"status,omitempty"` ApiServerUrl *string `json:"api_server_url,omitempty"` + // Default GatewayProfile identifier inherited by gateways created on this cluster when they do not supply their own profile_id + ProfileId *string `json:"profile_id,omitempty"` + // Default ManagedDatabase identifier for gateways placed on this cluster + DatabaseId *string `json:"database_id,omitempty"` } type _ManagedCluster ManagedCluster @@ -385,6 +389,70 @@ func (o *ManagedCluster) SetApiServerUrl(v string) { o.ApiServerUrl = &v } +// GetProfileId returns the ProfileId field value if set, zero value otherwise. +func (o *ManagedCluster) GetProfileId() string { + if o == nil || IsNil(o.ProfileId) { + var ret string + return ret + } + return *o.ProfileId +} + +// GetProfileIdOk returns a tuple with the ProfileId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManagedCluster) GetProfileIdOk() (*string, bool) { + if o == nil || IsNil(o.ProfileId) { + return nil, false + } + return o.ProfileId, true +} + +// HasProfileId returns a boolean if a field has been set. +func (o *ManagedCluster) HasProfileId() bool { + if o != nil && !IsNil(o.ProfileId) { + return true + } + + return false +} + +// SetProfileId gets a reference to the given string and assigns it to the ProfileId field. +func (o *ManagedCluster) SetProfileId(v string) { + o.ProfileId = &v +} + +// GetDatabaseId returns the DatabaseId field value if set, zero value otherwise. +func (o *ManagedCluster) GetDatabaseId() string { + if o == nil || IsNil(o.DatabaseId) { + var ret string + return ret + } + return *o.DatabaseId +} + +// GetDatabaseIdOk returns a tuple with the DatabaseId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManagedCluster) GetDatabaseIdOk() (*string, bool) { + if o == nil || IsNil(o.DatabaseId) { + return nil, false + } + return o.DatabaseId, true +} + +// HasDatabaseId returns a boolean if a field has been set. +func (o *ManagedCluster) HasDatabaseId() bool { + if o != nil && !IsNil(o.DatabaseId) { + return true + } + + return false +} + +// SetDatabaseId gets a reference to the given string and assigns it to the DatabaseId field. +func (o *ManagedCluster) SetDatabaseId(v string) { + o.DatabaseId = &v +} + func (o ManagedCluster) MarshalJSON() ([]byte, error) { toSerialize, err := o.ToMap() if err != nil { @@ -422,6 +490,12 @@ func (o ManagedCluster) ToMap() (map[string]interface{}, error) { if !IsNil(o.ApiServerUrl) { toSerialize["api_server_url"] = o.ApiServerUrl } + if !IsNil(o.ProfileId) { + toSerialize["profile_id"] = o.ProfileId + } + if !IsNil(o.DatabaseId) { + toSerialize["database_id"] = o.DatabaseId + } return toSerialize, nil } diff --git a/components/api-server/pkg/api/openapi/model_managed_cluster_patch_request.go b/components/api-server/pkg/api/openapi/model_managed_cluster_patch_request.go index dd244367..b6b1295d 100644 --- a/components/api-server/pkg/api/openapi/model_managed_cluster_patch_request.go +++ b/components/api-server/pkg/api/openapi/model_managed_cluster_patch_request.go @@ -25,6 +25,9 @@ type ManagedClusterPatchRequest struct { KubeconfigSecret *string `json:"kubeconfig_secret,omitempty"` Status *string `json:"status,omitempty"` ApiServerUrl *string `json:"api_server_url,omitempty"` + // Default GatewayProfile for gateways on this cluster; set to empty string to clear + ProfileId *string `json:"profile_id,omitempty"` + DatabaseId *string `json:"database_id,omitempty"` } // NewManagedClusterPatchRequest instantiates a new ManagedClusterPatchRequest object @@ -236,6 +239,70 @@ func (o *ManagedClusterPatchRequest) SetApiServerUrl(v string) { o.ApiServerUrl = &v } +// GetProfileId returns the ProfileId field value if set, zero value otherwise. +func (o *ManagedClusterPatchRequest) GetProfileId() string { + if o == nil || IsNil(o.ProfileId) { + var ret string + return ret + } + return *o.ProfileId +} + +// GetProfileIdOk returns a tuple with the ProfileId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManagedClusterPatchRequest) GetProfileIdOk() (*string, bool) { + if o == nil || IsNil(o.ProfileId) { + return nil, false + } + return o.ProfileId, true +} + +// HasProfileId returns a boolean if a field has been set. +func (o *ManagedClusterPatchRequest) HasProfileId() bool { + if o != nil && !IsNil(o.ProfileId) { + return true + } + + return false +} + +// SetProfileId gets a reference to the given string and assigns it to the ProfileId field. +func (o *ManagedClusterPatchRequest) SetProfileId(v string) { + o.ProfileId = &v +} + +// GetDatabaseId returns the DatabaseId field value if set, zero value otherwise. +func (o *ManagedClusterPatchRequest) GetDatabaseId() string { + if o == nil || IsNil(o.DatabaseId) { + var ret string + return ret + } + return *o.DatabaseId +} + +// GetDatabaseIdOk returns a tuple with the DatabaseId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *ManagedClusterPatchRequest) GetDatabaseIdOk() (*string, bool) { + if o == nil || IsNil(o.DatabaseId) { + return nil, false + } + return o.DatabaseId, true +} + +// HasDatabaseId returns a boolean if a field has been set. +func (o *ManagedClusterPatchRequest) HasDatabaseId() bool { + if o != nil && !IsNil(o.DatabaseId) { + return true + } + + return false +} + +// SetDatabaseId gets a reference to the given string and assigns it to the DatabaseId field. +func (o *ManagedClusterPatchRequest) SetDatabaseId(v string) { + o.DatabaseId = &v +} + func (o ManagedClusterPatchRequest) MarshalJSON() ([]byte, error) { toSerialize, err := o.ToMap() if err != nil { @@ -264,6 +331,12 @@ func (o ManagedClusterPatchRequest) ToMap() (map[string]interface{}, error) { if !IsNil(o.ApiServerUrl) { toSerialize["api_server_url"] = o.ApiServerUrl } + if !IsNil(o.ProfileId) { + toSerialize["profile_id"] = o.ProfileId + } + if !IsNil(o.DatabaseId) { + toSerialize["database_id"] = o.DatabaseId + } return toSerialize, nil } diff --git a/components/api-server/pkg/api/openapi_embed_test.go b/components/api-server/pkg/api/openapi_embed_test.go index f2437bbe..0986354b 100644 --- a/components/api-server/pkg/api/openapi_embed_test.go +++ b/components/api-server/pkg/api/openapi_embed_test.go @@ -55,14 +55,15 @@ func TestGetOpenAPISpecReturnsCompleteContract(t *testing.T) { operationIDs[operation.OperationID] = method + " " + path } } - if operationCount != 37 { - t.Fatalf("embedded operation count = %d, want 37", operationCount) + if operationCount != 42 { + t.Fatalf("embedded operation count = %d, want 42", operationCount) } expectedDeletes := map[string]string{ "/api/hypershell/v1/gateway_networks/{id}": "deleteGatewayNetwork", "/api/hypershell/v1/gateway_releases/{id}": "deleteGatewayRelease", "/api/hypershell/v1/gateways/{id}": "deleteGateway", + "/api/hypershell/v1/gateway_profiles/{id}": "deleteGatewayProfile", "/api/hypershell/v1/managed_clusters/{id}": "deleteManagedCluster", "/api/hypershell/v1/managed_databases/{id}": "deleteManagedDatabase", "/api/hypershell/v1/role_bindings/{id}": "deleteRoleBinding", diff --git a/components/api-server/pkg/rbac/authorization.go b/components/api-server/pkg/rbac/authorization.go index cb717e2e..0965afa7 100644 --- a/components/api-server/pkg/rbac/authorization.go +++ b/components/api-server/pkg/rbac/authorization.go @@ -206,6 +206,17 @@ func isAuthorized(method string, resource string, resourceID string, gatewayID s return len(bindings) > 0 } + if resource == "gateway_profiles" { + // A GatewayProfile defines the resource ceiling the control plane + // enforces, so mutating one must be restricted to platform admins. + // Reads are open to any authenticated caller holding a binding so that + // gateway creators/owners can view profiles to assign them. + if method == http.MethodGet { + return len(bindings) > 0 + } + return hasPlatformAdmin(bindings) + } + return hasGatewayCreator(bindings) } diff --git a/components/api-server/pkg/rbac/authorization_test.go b/components/api-server/pkg/rbac/authorization_test.go index 517a7bee..84bff8a2 100644 --- a/components/api-server/pkg/rbac/authorization_test.go +++ b/components/api-server/pkg/rbac/authorization_test.go @@ -178,6 +178,67 @@ func TestIsAuthorized_GatewayCreatorCanAccessGatewayReleases(t *testing.T) { } } +func TestIsAuthorized_PlatformAdminCanMutateGatewayProfiles(t *testing.T) { + bindings := []BindingSummary{ + {RoleName: "platform:admin", Scope: "global"}, + } + + for _, method := range []string{http.MethodPost, http.MethodPatch, http.MethodDelete} { + if !isAuthorized(method, "gateway_profiles", "", "", bindings) { + t.Errorf("platform:admin should be authorized for %s /gateway_profiles", method) + } + } +} + +func TestIsAuthorized_GatewayCreatorCannotMutateGatewayProfiles(t *testing.T) { + bindings := []BindingSummary{ + {RoleName: "gateway:creator", Scope: "global"}, + } + + for _, method := range []string{http.MethodPost, http.MethodPatch, http.MethodDelete} { + if isAuthorized(method, "gateway_profiles", "", "", bindings) { + t.Errorf("gateway:creator must not be authorized for %s /gateway_profiles", method) + } + } +} + +func TestIsAuthorized_GatewayOwnerCannotMutateGatewayProfiles(t *testing.T) { + bindings := []BindingSummary{ + {RoleName: "gateway:owner", Scope: "gateway", GatewayID: strPtr("gw-1")}, + } + + if isAuthorized(http.MethodPatch, "gateway_profiles", "gp-1", "", bindings) { + t.Error("gateway:owner must not PATCH gateway_profiles") + } + if isAuthorized(http.MethodDelete, "gateway_profiles", "gp-1", "", bindings) { + t.Error("gateway:owner must not DELETE gateway_profiles") + } +} + +func TestIsAuthorized_AnyBindingCanReadGatewayProfiles(t *testing.T) { + cases := [][]BindingSummary{ + {{RoleName: "gateway:creator", Scope: "global"}}, + {{RoleName: "gateway:owner", Scope: "gateway", GatewayID: strPtr("gw-1")}}, + {{RoleName: "gateway:viewer", Scope: "gateway", GatewayID: strPtr("gw-1")}}, + {{RoleName: "platform:admin", Scope: "global"}}, + } + + for _, bindings := range cases { + if !isAuthorized(http.MethodGet, "gateway_profiles", "", "", bindings) { + t.Errorf("%s should be authorized to list gateway_profiles", bindings[0].RoleName) + } + if !isAuthorized(http.MethodGet, "gateway_profiles", "gp-1", "", bindings) { + t.Errorf("%s should be authorized to get a gateway_profile", bindings[0].RoleName) + } + } +} + +func TestIsAuthorized_NoBindingsCannotReadGatewayProfiles(t *testing.T) { + if isAuthorized(http.MethodGet, "gateway_profiles", "", "", []BindingSummary{}) { + t.Error("empty bindings must not read gateway_profiles") + } +} + func strPtr(s string) *string { return &s } diff --git a/components/api-server/pkg/rbac/grpc_interceptor.go b/components/api-server/pkg/rbac/grpc_interceptor.go index 2056a013..e335f767 100644 --- a/components/api-server/pkg/rbac/grpc_interceptor.go +++ b/components/api-server/pkg/rbac/grpc_interceptor.go @@ -137,6 +137,17 @@ func isGRPCAuthorized(fullMethod string, bindings []BindingSummary) bool { return false } + // GatewayProfile mutations define the enforcement ceiling and must be + // restricted to platform admins, mirroring the REST rule in isAuthorized. + // Reads stay open to any caller holding a binding so creators/owners can + // view profiles to assign them. + if strings.Contains(fullMethod, "GatewayProfileService/") { + if isGRPCReadMethod(fullMethod) { + return true + } + return hasPlatformAdmin(bindings) + } + for _, b := range bindings { if b.RoleName == "platform:admin" { if isGRPCReadMethod(fullMethod) || isGRPCDeleteMethod(fullMethod) { diff --git a/components/api-server/pkg/rbac/grpc_interceptor_test.go b/components/api-server/pkg/rbac/grpc_interceptor_test.go index b0c833b6..83ba60ee 100644 --- a/components/api-server/pkg/rbac/grpc_interceptor_test.go +++ b/components/api-server/pkg/rbac/grpc_interceptor_test.go @@ -119,6 +119,59 @@ func TestIsGRPCAuthorized_NoBindingsDenied(t *testing.T) { } } +func TestIsGRPCAuthorized_PlatformAdminCanMutateGatewayProfiles(t *testing.T) { + bindings := []BindingSummary{ + {RoleName: "platform:admin", Scope: "global"}, + } + + for _, method := range []string{ + "/hypershell.v1.GatewayProfileService/CreateGatewayProfile", + "/hypershell.v1.GatewayProfileService/UpdateGatewayProfile", + "/hypershell.v1.GatewayProfileService/DeleteGatewayProfile", + } { + if !isGRPCAuthorized(method, bindings) { + t.Errorf("platform:admin should be authorized for %s", method) + } + } +} + +func TestIsGRPCAuthorized_CreatorCannotMutateGatewayProfiles(t *testing.T) { + bindings := []BindingSummary{ + {RoleName: "gateway:creator", Scope: "global"}, + } + + for _, method := range []string{ + "/hypershell.v1.GatewayProfileService/CreateGatewayProfile", + "/hypershell.v1.GatewayProfileService/UpdateGatewayProfile", + "/hypershell.v1.GatewayProfileService/DeleteGatewayProfile", + } { + if isGRPCAuthorized(method, bindings) { + t.Errorf("gateway:creator must not be authorized for %s", method) + } + } + + // but reads stay open + if !isGRPCAuthorized("/hypershell.v1.GatewayProfileService/ListGatewayProfiles", bindings) { + t.Error("gateway:creator should be able to list gateway profiles") + } + if !isGRPCAuthorized("/hypershell.v1.GatewayProfileService/GetGatewayProfile", bindings) { + t.Error("gateway:creator should be able to get a gateway profile") + } +} + +func TestIsGRPCAuthorized_ViewerCanReadGatewayProfiles(t *testing.T) { + bindings := []BindingSummary{ + {RoleName: "gateway:viewer", Scope: "gateway", GatewayID: strPtr("gw-1")}, + } + + if !isGRPCAuthorized("/hypershell.v1.GatewayProfileService/ListGatewayProfiles", bindings) { + t.Error("gateway:viewer should be able to list gateway profiles") + } + if isGRPCAuthorized("/hypershell.v1.GatewayProfileService/CreateGatewayProfile", bindings) { + t.Error("gateway:viewer must not create gateway profiles") + } +} + func TestIsServiceAccount_MatchesConfiguredAccount(t *testing.T) { accounts := []string{"service-account-hypershell-control-plane"} if !isServiceAccount("service-account-hypershell-control-plane", accounts) { diff --git a/components/api-server/plugins/gatewayProfiles/dao.go b/components/api-server/plugins/gatewayProfiles/dao.go new file mode 100644 index 00000000..2d531c73 --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/dao.go @@ -0,0 +1,107 @@ +package gatewayProfiles + +import ( + "context" + + "gorm.io/gorm/clause" + + "github.com/openshift-online/rh-trex-ai/pkg/api" + "github.com/openshift-online/rh-trex-ai/pkg/db" +) + +type GatewayProfileDao interface { + Get(ctx context.Context, id string) (*GatewayProfile, error) + Create(ctx context.Context, gatewayProfile *GatewayProfile) (*GatewayProfile, error) + Replace(ctx context.Context, gatewayProfile *GatewayProfile) (*GatewayProfile, error) + Delete(ctx context.Context, id string) error + FindByIDs(ctx context.Context, ids []string) (GatewayProfileList, error) + All(ctx context.Context) (GatewayProfileList, error) + // ExistsByClusterProfileID reports whether any live ManagedCluster references + // this profile as its default (managed_clusters.profile_id). + ExistsByClusterProfileID(ctx context.Context, profileID string) (bool, error) + // ExistsByGatewayProfileID reports whether any live Gateway references this + // profile (gateways.profile_id). + ExistsByGatewayProfileID(ctx context.Context, profileID string) (bool, error) +} + +var _ GatewayProfileDao = &sqlGatewayProfileDao{} + +type sqlGatewayProfileDao struct { + sessionFactory *db.SessionFactory +} + +func NewGatewayProfileDao(sessionFactory *db.SessionFactory) GatewayProfileDao { + return &sqlGatewayProfileDao{sessionFactory: sessionFactory} +} + +func (d *sqlGatewayProfileDao) Get(ctx context.Context, id string) (*GatewayProfile, error) { + g2 := (*d.sessionFactory).New(ctx) + var gatewayProfile GatewayProfile + if err := g2.Take(&gatewayProfile, "id = ?", id).Error; err != nil { + return nil, err + } + return &gatewayProfile, nil +} + +func (d *sqlGatewayProfileDao) Create(ctx context.Context, gatewayProfile *GatewayProfile) (*GatewayProfile, error) { + g2 := (*d.sessionFactory).New(ctx) + if err := g2.Omit(clause.Associations).Create(gatewayProfile).Error; err != nil { + db.MarkForRollback(ctx, err) + return nil, err + } + return gatewayProfile, nil +} + +func (d *sqlGatewayProfileDao) Replace(ctx context.Context, gatewayProfile *GatewayProfile) (*GatewayProfile, error) { + g2 := (*d.sessionFactory).New(ctx) + if err := g2.Omit(clause.Associations).Save(gatewayProfile).Error; err != nil { + db.MarkForRollback(ctx, err) + return nil, err + } + return gatewayProfile, nil +} + +func (d *sqlGatewayProfileDao) Delete(ctx context.Context, id string) error { + g2 := (*d.sessionFactory).New(ctx) + if err := g2.Omit(clause.Associations).Delete(&GatewayProfile{Meta: api.Meta{ID: id}}).Error; err != nil { + db.MarkForRollback(ctx, err) + return err + } + return nil +} + +func (d *sqlGatewayProfileDao) FindByIDs(ctx context.Context, ids []string) (GatewayProfileList, error) { + g2 := (*d.sessionFactory).New(ctx) + gatewayProfiles := GatewayProfileList{} + if err := g2.Where("id in (?)", ids).Find(&gatewayProfiles).Error; err != nil { + return nil, err + } + return gatewayProfiles, nil +} + +func (d *sqlGatewayProfileDao) All(ctx context.Context) (GatewayProfileList, error) { + g2 := (*d.sessionFactory).New(ctx) + gatewayProfiles := GatewayProfileList{} + if err := g2.Find(&gatewayProfiles).Error; err != nil { + return nil, err + } + return gatewayProfiles, nil +} + +func (d *sqlGatewayProfileDao) ExistsByClusterProfileID(ctx context.Context, profileID string) (bool, error) { + g2 := (*d.sessionFactory).New(ctx) + var count int64 + if err := g2.Raw("SELECT COUNT(*) FROM managed_clusters WHERE profile_id = ? AND deleted_at IS NULL", profileID).Scan(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} + +func (d *sqlGatewayProfileDao) ExistsByGatewayProfileID(ctx context.Context, profileID string) (bool, error) { + g2 := (*d.sessionFactory).New(ctx) + var count int64 + if err := g2.Raw("SELECT COUNT(*) FROM gateways WHERE profile_id = ? AND deleted_at IS NULL", profileID).Scan(&count).Error; err != nil { + return false, err + } + return count > 0, nil +} diff --git a/components/api-server/plugins/gatewayProfiles/grpc_handler.go b/components/api-server/plugins/gatewayProfiles/grpc_handler.go new file mode 100644 index 00000000..b52a0c86 --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/grpc_handler.go @@ -0,0 +1,155 @@ +package gatewayProfiles + +import ( + "context" + + pb "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1" + "github.com/openshift-online/rh-trex-ai/pkg/server/grpcutil" + "github.com/openshift-online/rh-trex-ai/pkg/services" +) + +type gatewayProfileGRPCHandler struct { + pb.UnimplementedGatewayProfileServiceServer + service GatewayProfileService + generic services.GenericService +} + +func NewGatewayProfileGRPCHandler(svc GatewayProfileService, generic services.GenericService) pb.GatewayProfileServiceServer { + return &gatewayProfileGRPCHandler{service: svc, generic: generic} +} + +func (h *gatewayProfileGRPCHandler) GetGatewayProfile(ctx context.Context, req *pb.GetGatewayProfileRequest) (*pb.GetGatewayProfileResponse, error) { + if err := grpcutil.ValidateRequiredID(req.Id); err != nil { + return nil, err + } + + gatewayProfile, svcErr := h.service.Get(ctx, req.Id) + if svcErr != nil { + return nil, grpcutil.ServiceErrorToGRPC(svcErr) + } + return &pb.GetGatewayProfileResponse{GatewayProfile: gatewayProfileToProto(gatewayProfile)}, nil +} + +func (h *gatewayProfileGRPCHandler) CreateGatewayProfile(ctx context.Context, req *pb.CreateGatewayProfileRequest) (*pb.CreateGatewayProfileResponse, error) { + if err := grpcutil.ValidateStringField("name", req.Name, true); err != nil { + return nil, err + } + + gatewayProfile := &GatewayProfile{ + Name: req.Name, + Description: req.Description, + CpuRequestTotal: req.CpuRequestTotal, + CpuLimitTotal: req.CpuLimitTotal, + MemoryRequestTotal: req.MemoryRequestTotal, + MemoryLimitTotal: req.MemoryLimitTotal, + EphemeralStorageTotal: req.EphemeralStorageTotal, + PodCount: req.PodCount, + PvcCount: req.PvcCount, + ContainerCpuRequestDefault: req.ContainerCpuRequestDefault, + ContainerCpuLimitMax: req.ContainerCpuLimitMax, + ContainerMemoryRequestDefault: req.ContainerMemoryRequestDefault, + ContainerMemoryLimitMax: req.ContainerMemoryLimitMax, + } + result, svcErr := h.service.Create(ctx, gatewayProfile) + if svcErr != nil { + return nil, grpcutil.ServiceErrorToGRPC(svcErr) + } + return &pb.CreateGatewayProfileResponse{GatewayProfile: gatewayProfileToProto(result)}, nil +} + +func (h *gatewayProfileGRPCHandler) UpdateGatewayProfile(ctx context.Context, req *pb.UpdateGatewayProfileRequest) (*pb.UpdateGatewayProfileResponse, error) { + if err := grpcutil.ValidateRequiredID(req.Id); err != nil { + return nil, err + } + if req.Name != nil { + if err := grpcutil.ValidateStringField("name", *req.Name, false); err != nil { + return nil, err + } + } + + gatewayProfile, svcErr := h.service.Get(ctx, req.Id) + if svcErr != nil { + return nil, grpcutil.ServiceErrorToGRPC(svcErr) + } + if req.Name != nil { + gatewayProfile.Name = *req.Name + } + if req.Description != nil { + gatewayProfile.Description = req.Description + } + if req.CpuRequestTotal != nil { + gatewayProfile.CpuRequestTotal = req.CpuRequestTotal + } + if req.CpuLimitTotal != nil { + gatewayProfile.CpuLimitTotal = req.CpuLimitTotal + } + if req.MemoryRequestTotal != nil { + gatewayProfile.MemoryRequestTotal = req.MemoryRequestTotal + } + if req.MemoryLimitTotal != nil { + gatewayProfile.MemoryLimitTotal = req.MemoryLimitTotal + } + if req.EphemeralStorageTotal != nil { + gatewayProfile.EphemeralStorageTotal = req.EphemeralStorageTotal + } + if req.PodCount != nil { + gatewayProfile.PodCount = req.PodCount + } + if req.PvcCount != nil { + gatewayProfile.PvcCount = req.PvcCount + } + if req.ContainerCpuRequestDefault != nil { + gatewayProfile.ContainerCpuRequestDefault = req.ContainerCpuRequestDefault + } + if req.ContainerCpuLimitMax != nil { + gatewayProfile.ContainerCpuLimitMax = req.ContainerCpuLimitMax + } + if req.ContainerMemoryRequestDefault != nil { + gatewayProfile.ContainerMemoryRequestDefault = req.ContainerMemoryRequestDefault + } + if req.ContainerMemoryLimitMax != nil { + gatewayProfile.ContainerMemoryLimitMax = req.ContainerMemoryLimitMax + } + result, svcErr := h.service.Replace(ctx, gatewayProfile) + if svcErr != nil { + return nil, grpcutil.ServiceErrorToGRPC(svcErr) + } + return &pb.UpdateGatewayProfileResponse{GatewayProfile: gatewayProfileToProto(result)}, nil +} + +func (h *gatewayProfileGRPCHandler) DeleteGatewayProfile(ctx context.Context, req *pb.DeleteGatewayProfileRequest) (*pb.DeleteGatewayProfileResponse, error) { + if err := grpcutil.ValidateRequiredID(req.Id); err != nil { + return nil, err + } + + svcErr := h.service.Delete(ctx, req.Id) + if svcErr != nil { + return nil, grpcutil.ServiceErrorToGRPC(svcErr) + } + return &pb.DeleteGatewayProfileResponse{}, nil +} + +func (h *gatewayProfileGRPCHandler) ListGatewayProfiles(ctx context.Context, req *pb.ListGatewayProfilesRequest) (*pb.ListGatewayProfilesResponse, error) { + page, size := grpcutil.NormalizePagination(req.Page, req.Size) + + listArgs := &services.ListArguments{ + Page: int(page), + Size: int64(size), + } + + var gatewayProfiles []GatewayProfile + paging, svcErr := h.generic.List(ctx, "id", listArgs, &gatewayProfiles) + if svcErr != nil { + return nil, grpcutil.ServiceErrorToGRPC(svcErr) + } + + items := make([]*pb.GatewayProfile, len(gatewayProfiles)) + for i, d := range gatewayProfiles { + items[i] = gatewayProfileToProto(&d) + } + + return &pb.ListGatewayProfilesResponse{ + Items: items, + Metadata: &pb.ListMeta{Page: page, Size: size, Total: int32(paging.Total)}, + }, nil +} diff --git a/components/api-server/plugins/gatewayProfiles/grpc_presenter.go b/components/api-server/plugins/gatewayProfiles/grpc_presenter.go new file mode 100644 index 00000000..4c7b49f6 --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/grpc_presenter.go @@ -0,0 +1,31 @@ +package gatewayProfiles + +import ( + pb "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func gatewayProfileToProto(d *GatewayProfile) *pb.GatewayProfile { + return &pb.GatewayProfile{ + Metadata: &pb.ObjectReference{ + Id: d.ID, + CreatedAt: timestamppb.New(d.CreatedAt), + UpdatedAt: timestamppb.New(d.UpdatedAt), + Kind: "GatewayProfile", + Href: "/api/hypershell/v1/gateway_profiles/" + d.ID, + }, + Name: d.Name, + Description: d.Description, + CpuRequestTotal: d.CpuRequestTotal, + CpuLimitTotal: d.CpuLimitTotal, + MemoryRequestTotal: d.MemoryRequestTotal, + MemoryLimitTotal: d.MemoryLimitTotal, + EphemeralStorageTotal: d.EphemeralStorageTotal, + PodCount: d.PodCount, + PvcCount: d.PvcCount, + ContainerCpuRequestDefault: d.ContainerCpuRequestDefault, + ContainerCpuLimitMax: d.ContainerCpuLimitMax, + ContainerMemoryRequestDefault: d.ContainerMemoryRequestDefault, + ContainerMemoryLimitMax: d.ContainerMemoryLimitMax, + } +} diff --git a/components/api-server/plugins/gatewayProfiles/handler.go b/components/api-server/plugins/gatewayProfiles/handler.go new file mode 100644 index 00000000..883376bd --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/handler.go @@ -0,0 +1,189 @@ +package gatewayProfiles + +import ( + "net/http" + + "github.com/gorilla/mux" + + "github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi" + "github.com/openshift-online/rh-trex-ai/pkg/api/presenters" + "github.com/openshift-online/rh-trex-ai/pkg/errors" + "github.com/openshift-online/rh-trex-ai/pkg/handlers" + "github.com/openshift-online/rh-trex-ai/pkg/services" +) + +var _ handlers.RestHandler = gatewayProfileHandler{} + +type gatewayProfileHandler struct { + gatewayProfile GatewayProfileService + generic services.GenericService +} + +func NewGatewayProfileHandler(gatewayProfile GatewayProfileService, generic services.GenericService) *gatewayProfileHandler { + return &gatewayProfileHandler{ + gatewayProfile: gatewayProfile, + generic: generic, + } +} + +func (h gatewayProfileHandler) Create(w http.ResponseWriter, r *http.Request) { + var gatewayProfile openapi.GatewayProfile + cfg := &handlers.HandlerConfig{ + Body: &gatewayProfile, + Validators: []handlers.Validate{ + handlers.ValidateEmpty(&gatewayProfile, "Id", "id"), + handlers.ValidateNotEmpty(&gatewayProfile, "Name", "name"), + }, + Action: func() (interface{}, *errors.ServiceError) { + ctx := r.Context() + gatewayProfileModel := ConvertGatewayProfile(gatewayProfile) + gatewayProfileModel, err := h.gatewayProfile.Create(ctx, gatewayProfileModel) + if err != nil { + return nil, err + } + return PresentGatewayProfile(gatewayProfileModel), nil + }, + ErrorHandler: handlers.HandleError, + } + + handlers.Handle(w, r, cfg, http.StatusCreated) +} + +func (h gatewayProfileHandler) Patch(w http.ResponseWriter, r *http.Request) { + var patch openapi.GatewayProfilePatchRequest + + cfg := &handlers.HandlerConfig{ + Body: &patch, + Validators: []handlers.Validate{}, + Action: func() (interface{}, *errors.ServiceError) { + ctx := r.Context() + id := mux.Vars(r)["id"] + found, err := h.gatewayProfile.Get(ctx, id) + if err != nil { + return nil, err + } + + if patch.Name != nil { + found.Name = *patch.Name + } + if patch.Description != nil { + found.Description = patch.Description + } + if patch.CpuRequestTotal != nil { + found.CpuRequestTotal = patch.CpuRequestTotal + } + if patch.CpuLimitTotal != nil { + found.CpuLimitTotal = patch.CpuLimitTotal + } + if patch.MemoryRequestTotal != nil { + found.MemoryRequestTotal = patch.MemoryRequestTotal + } + if patch.MemoryLimitTotal != nil { + found.MemoryLimitTotal = patch.MemoryLimitTotal + } + if patch.EphemeralStorageTotal != nil { + found.EphemeralStorageTotal = patch.EphemeralStorageTotal + } + if patch.PodCount != nil { + found.PodCount = patch.PodCount + } + if patch.PvcCount != nil { + found.PvcCount = patch.PvcCount + } + if patch.ContainerCpuRequestDefault != nil { + found.ContainerCpuRequestDefault = patch.ContainerCpuRequestDefault + } + if patch.ContainerCpuLimitMax != nil { + found.ContainerCpuLimitMax = patch.ContainerCpuLimitMax + } + if patch.ContainerMemoryRequestDefault != nil { + found.ContainerMemoryRequestDefault = patch.ContainerMemoryRequestDefault + } + if patch.ContainerMemoryLimitMax != nil { + found.ContainerMemoryLimitMax = patch.ContainerMemoryLimitMax + } + + gatewayProfileModel, err := h.gatewayProfile.Replace(ctx, found) + if err != nil { + return nil, err + } + return PresentGatewayProfile(gatewayProfileModel), nil + }, + ErrorHandler: handlers.HandleError, + } + + handlers.Handle(w, r, cfg, http.StatusOK) +} + +func (h gatewayProfileHandler) List(w http.ResponseWriter, r *http.Request) { + cfg := &handlers.HandlerConfig{ + Action: func() (interface{}, *errors.ServiceError) { + ctx := r.Context() + + listArgs := services.NewListArguments(r.URL.Query()) + var gatewayProfiles []GatewayProfile + paging, err := h.generic.List(ctx, "id", listArgs, &gatewayProfiles) + if err != nil { + return nil, err + } + kindStr := "GatewayProfileList" + pageVal := int32(paging.Page) + sizeVal := int32(paging.Size) + totalVal := int32(paging.Total) + gatewayProfileList := openapi.GatewayProfileList{ + Kind: &kindStr, + Page: &pageVal, + Size: &sizeVal, + Total: &totalVal, + Items: []openapi.GatewayProfile{}, + } + + for _, gatewayProfile := range gatewayProfiles { + converted := PresentGatewayProfile(&gatewayProfile) + gatewayProfileList.Items = append(gatewayProfileList.Items, converted) + } + if listArgs.Fields != nil { + filteredItems, err := presenters.SliceFilter(listArgs.Fields, gatewayProfileList.Items) + if err != nil { + return nil, err + } + return filteredItems, nil + } + return gatewayProfileList, nil + }, + } + + handlers.HandleList(w, r, cfg) +} + +func (h gatewayProfileHandler) Get(w http.ResponseWriter, r *http.Request) { + cfg := &handlers.HandlerConfig{ + Action: func() (interface{}, *errors.ServiceError) { + id := mux.Vars(r)["id"] + ctx := r.Context() + gatewayProfile, err := h.gatewayProfile.Get(ctx, id) + if err != nil { + return nil, err + } + + return PresentGatewayProfile(gatewayProfile), nil + }, + } + + handlers.HandleGet(w, r, cfg) +} + +func (h gatewayProfileHandler) Delete(w http.ResponseWriter, r *http.Request) { + cfg := &handlers.HandlerConfig{ + Action: func() (interface{}, *errors.ServiceError) { + id := mux.Vars(r)["id"] + ctx := r.Context() + err := h.gatewayProfile.Delete(ctx, id) + if err != nil { + return nil, err + } + return nil, nil + }, + } + handlers.HandleDelete(w, r, cfg, http.StatusNoContent) +} diff --git a/components/api-server/plugins/gatewayProfiles/integration_test.go b/components/api-server/plugins/gatewayProfiles/integration_test.go new file mode 100644 index 00000000..04bec89e --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/integration_test.go @@ -0,0 +1,138 @@ +package gatewayProfiles_test + +import ( + "fmt" + "net/http" + "testing" + + . "github.com/onsi/gomega" + + "github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi" + "github.com/openshift-online/hypershell/components/api-server/test" +) + +// TestGatewayProfileCRUD covers the create/get/list/patch/delete happy path +// through the generated OpenAPI client against a live API server. +func TestGatewayProfileCRUD(t *testing.T) { + h, client := test.RegisterIntegration(t) + + account := h.NewRandAccount() + ctx := h.NewAuthenticatedContext(account) + + input := openapi.GatewayProfile{ + Name: "small", + Description: openapi.PtrString("small workloads"), + CpuRequestTotal: openapi.PtrString("2"), + MemoryLimitTotal: openapi.PtrString("8Gi"), + PodCount: openapi.PtrInt32(10), + PvcCount: openapi.PtrInt32(2), + } + + created, resp, err := client.DefaultAPI.CreateGatewayProfile(ctx).GatewayProfile(input).Execute() + Expect(err).NotTo(HaveOccurred(), "Error creating gateway profile: %v", err) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + Expect(*created.Id).NotTo(BeEmpty(), "Expected ID assigned on creation") + Expect(*created.Kind).To(Equal("GatewayProfile")) + Expect(*created.Href).To(Equal(fmt.Sprintf("/api/hypershell/v1/gateway_profiles/%s", *created.Id))) + Expect(created.Name).To(Equal("small")) + + got, resp, err := client.DefaultAPI.GetGatewayProfile(ctx, *created.Id).Execute() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(*got.Id).To(Equal(*created.Id)) + Expect(got.GetMemoryLimitTotal()).To(Equal("8Gi")) + + list, _, err := client.DefaultAPI.ListGatewayProfiles(ctx).Execute() + Expect(err).NotTo(HaveOccurred()) + Expect(list.GetTotal()).To(BeNumerically(">=", int32(1))) + + patched, resp, err := client.DefaultAPI.UpdateGatewayProfile(ctx, *created.Id). + GatewayProfilePatchRequest(openapi.GatewayProfilePatchRequest{Description: openapi.PtrString("updated")}). + Execute() + Expect(err).NotTo(HaveOccurred(), "Error patching gateway profile: %v", err) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(patched.GetDescription()).To(Equal("updated")) + + resp, err = client.DefaultAPI.DeleteGatewayProfile(ctx, *created.Id).Execute() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) + + _, resp, err = client.DefaultAPI.GetGatewayProfile(ctx, *created.Id).Execute() + Expect(err).To(HaveOccurred(), "Expected deleted gateway profile to return 404") + Expect(resp.StatusCode).To(Equal(http.StatusNotFound)) +} + +// TestGatewayProfileValidationRejected covers boundary validation: an invalid +// Kubernetes resource quantity must be rejected with HTTP 400 before storage. +func TestGatewayProfileValidationRejected(t *testing.T) { + h, client := test.RegisterIntegration(t) + + account := h.NewRandAccount() + ctx := h.NewAuthenticatedContext(account) + + input := openapi.GatewayProfile{ + Name: "invalid", + MemoryLimitTotal: openapi.PtrString("8GB"), // "GB" is not a valid quantity suffix + } + + _, resp, err := client.DefaultAPI.CreateGatewayProfile(ctx).GatewayProfile(input).Execute() + Expect(err).To(HaveOccurred(), "Expected invalid quantity to be rejected") + Expect(resp.StatusCode).To(Equal(http.StatusBadRequest)) +} + +// TestGatewayProfileMissingNameRejected covers required-field validation: a +// create without a name must be rejected with HTTP 400, matching the gRPC +// CreateGatewayProfile contract which requires name. +func TestGatewayProfileMissingNameRejected(t *testing.T) { + h, client := test.RegisterIntegration(t) + + account := h.NewRandAccount() + ctx := h.NewAuthenticatedContext(account) + + input := openapi.GatewayProfile{ + CpuRequestTotal: openapi.PtrString("1"), + } + + _, resp, err := client.DefaultAPI.CreateGatewayProfile(ctx).GatewayProfile(input).Execute() + Expect(err).To(HaveOccurred(), "Expected missing name to be rejected") + Expect(resp.StatusCode).To(Equal(http.StatusBadRequest)) +} + +// TestGatewayProfileDeletionProtectedByGateway covers deletion protection: a +// profile referenced by a live gateway must not be deleted (HTTP 409). +func TestGatewayProfileDeletionProtectedByGateway(t *testing.T) { + h, client := test.RegisterIntegration(t) + + account := h.NewRandAccount() + ctx := h.NewAuthenticatedContext(account) + + profile, resp, err := client.DefaultAPI.CreateGatewayProfile(ctx). + GatewayProfile(openapi.GatewayProfile{Name: "referenced", CpuRequestTotal: openapi.PtrString("1")}). + Execute() + Expect(err).NotTo(HaveOccurred(), "Error creating gateway profile: %v", err) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + + // A gateway referencing the profile makes it undeletable. Placement assigns + // database_id server-side, so empty placement IDs are acceptable here. + gatewayInput := openapi.GatewayCreateRequest{ + Name: "profile-ref-gw", + ProfileId: openapi.PtrString(*profile.Id), + } + gateway, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(gatewayInput).Execute() + Expect(err).NotTo(HaveOccurred(), "Error creating gateway referencing the profile: %v", err) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + Expect(gateway.GetProfileId()).To(Equal(*profile.Id)) + + resp, err = client.DefaultAPI.DeleteGatewayProfile(ctx, *profile.Id).Execute() + Expect(err).To(HaveOccurred(), "Expected referenced profile deletion to be rejected") + Expect(resp.StatusCode).To(Equal(http.StatusConflict)) + + // After removing the referencing gateway the profile becomes deletable. + resp, err = client.DefaultAPI.DeleteGateway(ctx, *gateway.Id).Execute() + Expect(err).NotTo(HaveOccurred()) + Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) + + resp, err = client.DefaultAPI.DeleteGatewayProfile(ctx, *profile.Id).Execute() + Expect(err).NotTo(HaveOccurred(), "Expected unreferenced profile to be deletable") + Expect(resp.StatusCode).To(Equal(http.StatusNoContent)) +} diff --git a/components/api-server/plugins/gatewayProfiles/migration.go b/components/api-server/plugins/gatewayProfiles/migration.go new file mode 100644 index 00000000..7a396c6f --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/migration.go @@ -0,0 +1,37 @@ +package gatewayProfiles + +import ( + "gorm.io/gorm" + + "github.com/go-gormigrate/gormigrate/v2" + "github.com/openshift-online/rh-trex-ai/pkg/db" +) + +func migration() *gormigrate.Migration { + type GatewayProfile struct { + db.Model + Name string + Description *string + CpuRequestTotal *string + CpuLimitTotal *string + MemoryRequestTotal *string + MemoryLimitTotal *string + EphemeralStorageTotal *string + PodCount *int32 + PvcCount *int32 + ContainerCpuRequestDefault *string + ContainerCpuLimitMax *string + ContainerMemoryRequestDefault *string + ContainerMemoryLimitMax *string + } + + return &gormigrate.Migration{ + ID: "2026082800000001", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate(&GatewayProfile{}) + }, + Rollback: func(tx *gorm.DB) error { + return tx.Migrator().DropTable(&GatewayProfile{}) + }, + } +} diff --git a/components/api-server/plugins/gatewayProfiles/mock_dao.go b/components/api-server/plugins/gatewayProfiles/mock_dao.go new file mode 100644 index 00000000..09d43634 --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/mock_dao.go @@ -0,0 +1,57 @@ +package gatewayProfiles + +import ( + "context" + + "gorm.io/gorm" + + "github.com/openshift-online/rh-trex-ai/pkg/errors" +) + +var _ GatewayProfileDao = &gatewayProfileDaoMock{} + +type gatewayProfileDaoMock struct { + gatewayProfiles GatewayProfileList +} + +func NewMockGatewayProfileDao() *gatewayProfileDaoMock { + return &gatewayProfileDaoMock{} +} + +func (d *gatewayProfileDaoMock) Get(ctx context.Context, id string) (*GatewayProfile, error) { + for _, gatewayProfile := range d.gatewayProfiles { + if gatewayProfile.ID == id { + return gatewayProfile, nil + } + } + return nil, gorm.ErrRecordNotFound +} + +func (d *gatewayProfileDaoMock) Create(ctx context.Context, gatewayProfile *GatewayProfile) (*GatewayProfile, error) { + d.gatewayProfiles = append(d.gatewayProfiles, gatewayProfile) + return gatewayProfile, nil +} + +func (d *gatewayProfileDaoMock) Replace(ctx context.Context, gatewayProfile *GatewayProfile) (*GatewayProfile, error) { + return nil, errors.NotImplemented("GatewayProfile").AsError() +} + +func (d *gatewayProfileDaoMock) Delete(ctx context.Context, id string) error { + return errors.NotImplemented("GatewayProfile").AsError() +} + +func (d *gatewayProfileDaoMock) FindByIDs(ctx context.Context, ids []string) (GatewayProfileList, error) { + return nil, errors.NotImplemented("GatewayProfile").AsError() +} + +func (d *gatewayProfileDaoMock) All(ctx context.Context) (GatewayProfileList, error) { + return d.gatewayProfiles, nil +} + +func (d *gatewayProfileDaoMock) ExistsByClusterProfileID(ctx context.Context, profileID string) (bool, error) { + return false, nil +} + +func (d *gatewayProfileDaoMock) ExistsByGatewayProfileID(ctx context.Context, profileID string) (bool, error) { + return false, nil +} diff --git a/components/api-server/plugins/gatewayProfiles/model.go b/components/api-server/plugins/gatewayProfiles/model.go new file mode 100644 index 00000000..cc9bb3cd --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/model.go @@ -0,0 +1,55 @@ +package gatewayProfiles + +import ( + "github.com/openshift-online/rh-trex-ai/pkg/api" + "gorm.io/gorm" +) + +type GatewayProfile struct { + api.Meta + Name string `json:"name"` + Description *string `json:"description"` + CpuRequestTotal *string `json:"cpu_request_total"` + CpuLimitTotal *string `json:"cpu_limit_total"` + MemoryRequestTotal *string `json:"memory_request_total"` + MemoryLimitTotal *string `json:"memory_limit_total"` + EphemeralStorageTotal *string `json:"ephemeral_storage_total"` + PodCount *int32 `json:"pod_count"` + PvcCount *int32 `json:"pvc_count"` + ContainerCpuRequestDefault *string `json:"container_cpu_request_default"` + ContainerCpuLimitMax *string `json:"container_cpu_limit_max"` + ContainerMemoryRequestDefault *string `json:"container_memory_request_default"` + ContainerMemoryLimitMax *string `json:"container_memory_limit_max"` +} + +type GatewayProfileList []*GatewayProfile +type GatewayProfileIndex map[string]*GatewayProfile + +func (l GatewayProfileList) Index() GatewayProfileIndex { + index := GatewayProfileIndex{} + for _, o := range l { + index[o.ID] = o + } + return index +} + +func (d *GatewayProfile) BeforeCreate(tx *gorm.DB) error { + d.ID = api.NewID() + return nil +} + +type GatewayProfilePatchRequest struct { + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + CpuRequestTotal *string `json:"cpu_request_total,omitempty"` + CpuLimitTotal *string `json:"cpu_limit_total,omitempty"` + MemoryRequestTotal *string `json:"memory_request_total,omitempty"` + MemoryLimitTotal *string `json:"memory_limit_total,omitempty"` + EphemeralStorageTotal *string `json:"ephemeral_storage_total,omitempty"` + PodCount *int32 `json:"pod_count,omitempty"` + PvcCount *int32 `json:"pvc_count,omitempty"` + ContainerCpuRequestDefault *string `json:"container_cpu_request_default,omitempty"` + ContainerCpuLimitMax *string `json:"container_cpu_limit_max,omitempty"` + ContainerMemoryRequestDefault *string `json:"container_memory_request_default,omitempty"` + ContainerMemoryLimitMax *string `json:"container_memory_limit_max,omitempty"` +} diff --git a/components/api-server/plugins/gatewayProfiles/plugin.go b/components/api-server/plugins/gatewayProfiles/plugin.go new file mode 100644 index 00000000..c2b12f11 --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/plugin.go @@ -0,0 +1,90 @@ +package gatewayProfiles + +import ( + "net/http" + + "github.com/gorilla/mux" + "google.golang.org/grpc" + + pb "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1" + "github.com/openshift-online/rh-trex-ai/pkg/api" + "github.com/openshift-online/rh-trex-ai/pkg/api/presenters" + "github.com/openshift-online/rh-trex-ai/pkg/auth" + "github.com/openshift-online/rh-trex-ai/pkg/controllers" + "github.com/openshift-online/rh-trex-ai/pkg/db" + "github.com/openshift-online/rh-trex-ai/pkg/environments" + "github.com/openshift-online/rh-trex-ai/pkg/registry" + pkgserver "github.com/openshift-online/rh-trex-ai/pkg/server" + "github.com/openshift-online/rh-trex-ai/plugins/events" + "github.com/openshift-online/rh-trex-ai/plugins/generic" +) + +type ServiceLocator func() GatewayProfileService + +func NewServiceLocator(env *environments.Env) ServiceLocator { + return func() GatewayProfileService { + return NewGatewayProfileService( + db.NewAdvisoryLockFactory(env.Database.SessionFactory), + NewGatewayProfileDao(&env.Database.SessionFactory), + events.Service(&env.Services), + ) + } +} + +func Service(s *environments.Services) GatewayProfileService { + if s == nil { + return nil + } + if obj := s.GetService("GatewayProfiles"); obj != nil { + locator := obj.(ServiceLocator) + return locator() + } + return nil +} + +func init() { + registry.RegisterService("GatewayProfiles", func(env interface{}) interface{} { + return NewServiceLocator(env.(*environments.Env)) + }) + + pkgserver.RegisterRoutes("gatewayProfiles", func(apiV1Router *mux.Router, services pkgserver.ServicesInterface, authMiddleware environments.JWTMiddleware, authzMiddleware auth.AuthorizationMiddleware) { + envServices := services.(*environments.Services) + gatewayProfileHandler := NewGatewayProfileHandler(Service(envServices), generic.Service(envServices)) + + gatewayProfilesRouter := apiV1Router.PathPrefix("/gateway_profiles").Subrouter() + gatewayProfilesRouter.HandleFunc("", gatewayProfileHandler.List).Methods(http.MethodGet) + gatewayProfilesRouter.HandleFunc("/{id}", gatewayProfileHandler.Get).Methods(http.MethodGet) + gatewayProfilesRouter.HandleFunc("", gatewayProfileHandler.Create).Methods(http.MethodPost) + gatewayProfilesRouter.HandleFunc("/{id}", gatewayProfileHandler.Patch).Methods(http.MethodPatch) + gatewayProfilesRouter.HandleFunc("/{id}", gatewayProfileHandler.Delete).Methods(http.MethodDelete) + gatewayProfilesRouter.Use(authMiddleware.AuthenticateAccountJWT) + gatewayProfilesRouter.Use(authzMiddleware.AuthorizeApi) + }) + + pkgserver.RegisterController("GatewayProfiles", func(manager *controllers.KindControllerManager, services pkgserver.ServicesInterface) { + gatewayProfileServices := Service(services.(*environments.Services)) + + manager.Add(&controllers.ControllerConfig{ + Source: "GatewayProfiles", + Handlers: map[api.EventType][]controllers.ControllerHandlerFunc{ + api.CreateEventType: {gatewayProfileServices.OnUpsert}, + api.UpdateEventType: {gatewayProfileServices.OnUpsert}, + api.DeleteEventType: {gatewayProfileServices.OnDelete}, + }, + }) + }) + + pkgserver.RegisterGRPCService("gatewayProfiles", func(grpcServer *grpc.Server, services pkgserver.ServicesInterface) { + envServices := services.(*environments.Services) + gatewayProfileService := Service(envServices) + genericService := generic.Service(envServices) + pb.RegisterGatewayProfileServiceServer(grpcServer, NewGatewayProfileGRPCHandler(gatewayProfileService, genericService)) + }) + + presenters.RegisterPath(GatewayProfile{}, "gateway_profiles") + presenters.RegisterPath(&GatewayProfile{}, "gateway_profiles") + presenters.RegisterKind(GatewayProfile{}, "GatewayProfile") + presenters.RegisterKind(&GatewayProfile{}, "GatewayProfile") + + db.RegisterMigration(migration()) +} diff --git a/components/api-server/plugins/gatewayProfiles/presenter.go b/components/api-server/plugins/gatewayProfiles/presenter.go new file mode 100644 index 00000000..2a7e25cd --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/presenter.go @@ -0,0 +1,60 @@ +package gatewayProfiles + +import ( + "github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi" + "github.com/openshift-online/rh-trex-ai/pkg/api" + "github.com/openshift-online/rh-trex-ai/pkg/api/presenters" + "github.com/openshift-online/rh-trex-ai/pkg/util" +) + +func ConvertGatewayProfile(gatewayProfile openapi.GatewayProfile) *GatewayProfile { + c := &GatewayProfile{ + Meta: api.Meta{ + ID: util.NilToEmptyString(gatewayProfile.Id), + }, + } + c.Name = gatewayProfile.Name + c.Description = gatewayProfile.Description + c.CpuRequestTotal = gatewayProfile.CpuRequestTotal + c.CpuLimitTotal = gatewayProfile.CpuLimitTotal + c.MemoryRequestTotal = gatewayProfile.MemoryRequestTotal + c.MemoryLimitTotal = gatewayProfile.MemoryLimitTotal + c.EphemeralStorageTotal = gatewayProfile.EphemeralStorageTotal + c.PodCount = gatewayProfile.PodCount + c.PvcCount = gatewayProfile.PvcCount + c.ContainerCpuRequestDefault = gatewayProfile.ContainerCpuRequestDefault + c.ContainerCpuLimitMax = gatewayProfile.ContainerCpuLimitMax + c.ContainerMemoryRequestDefault = gatewayProfile.ContainerMemoryRequestDefault + c.ContainerMemoryLimitMax = gatewayProfile.ContainerMemoryLimitMax + + if gatewayProfile.CreatedAt != nil { + c.CreatedAt = *gatewayProfile.CreatedAt + c.UpdatedAt = *gatewayProfile.UpdatedAt + } + + return c +} + +func PresentGatewayProfile(gatewayProfile *GatewayProfile) openapi.GatewayProfile { + reference := presenters.PresentReference(gatewayProfile.ID, gatewayProfile) + return openapi.GatewayProfile{ + Id: reference.Id, + Kind: reference.Kind, + Href: reference.Href, + CreatedAt: openapi.PtrTime(gatewayProfile.CreatedAt), + UpdatedAt: openapi.PtrTime(gatewayProfile.UpdatedAt), + Name: gatewayProfile.Name, + Description: gatewayProfile.Description, + CpuRequestTotal: gatewayProfile.CpuRequestTotal, + CpuLimitTotal: gatewayProfile.CpuLimitTotal, + MemoryRequestTotal: gatewayProfile.MemoryRequestTotal, + MemoryLimitTotal: gatewayProfile.MemoryLimitTotal, + EphemeralStorageTotal: gatewayProfile.EphemeralStorageTotal, + PodCount: gatewayProfile.PodCount, + PvcCount: gatewayProfile.PvcCount, + ContainerCpuRequestDefault: gatewayProfile.ContainerCpuRequestDefault, + ContainerCpuLimitMax: gatewayProfile.ContainerCpuLimitMax, + ContainerMemoryRequestDefault: gatewayProfile.ContainerMemoryRequestDefault, + ContainerMemoryLimitMax: gatewayProfile.ContainerMemoryLimitMax, + } +} diff --git a/components/api-server/plugins/gatewayProfiles/service.go b/components/api-server/plugins/gatewayProfiles/service.go new file mode 100644 index 00000000..7b67d35f --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/service.go @@ -0,0 +1,175 @@ +package gatewayProfiles + +import ( + "context" + + "github.com/openshift-online/rh-trex-ai/pkg/api" + "github.com/openshift-online/rh-trex-ai/pkg/db" + "github.com/openshift-online/rh-trex-ai/pkg/errors" + "github.com/openshift-online/rh-trex-ai/pkg/logger" + "github.com/openshift-online/rh-trex-ai/pkg/services" +) + +const gatewayProfilesLockType db.LockType = "gateway_profiles" + +type GatewayProfileService interface { + Get(ctx context.Context, id string) (*GatewayProfile, *errors.ServiceError) + Create(ctx context.Context, gatewayProfile *GatewayProfile) (*GatewayProfile, *errors.ServiceError) + Replace(ctx context.Context, gatewayProfile *GatewayProfile) (*GatewayProfile, *errors.ServiceError) + Delete(ctx context.Context, id string) *errors.ServiceError + All(ctx context.Context) (GatewayProfileList, *errors.ServiceError) + + FindByIDs(ctx context.Context, ids []string) (GatewayProfileList, *errors.ServiceError) + + OnUpsert(ctx context.Context, id string) error + OnDelete(ctx context.Context, id string) error +} + +func NewGatewayProfileService(lockFactory db.LockFactory, gatewayProfileDao GatewayProfileDao, events services.EventService) GatewayProfileService { + return &sqlGatewayProfileService{ + lockFactory: lockFactory, + gatewayProfileDao: gatewayProfileDao, + events: events, + } +} + +var _ GatewayProfileService = &sqlGatewayProfileService{} + +type sqlGatewayProfileService struct { + lockFactory db.LockFactory + gatewayProfileDao GatewayProfileDao + events services.EventService +} + +func (s *sqlGatewayProfileService) OnUpsert(ctx context.Context, id string) error { + logger := logger.NewLogger(ctx) + + gatewayProfile, err := s.gatewayProfileDao.Get(ctx, id) + if err != nil { + return err + } + + logger.Infof("GatewayProfile upserted: %s (name=%s)", gatewayProfile.ID, gatewayProfile.Name) + + return nil +} + +func (s *sqlGatewayProfileService) OnDelete(ctx context.Context, id string) error { + logger := logger.NewLogger(ctx) + logger.Infof("This gatewayProfile has been deleted: %s", id) + return nil +} + +func (s *sqlGatewayProfileService) Get(ctx context.Context, id string) (*GatewayProfile, *errors.ServiceError) { + gatewayProfile, err := s.gatewayProfileDao.Get(ctx, id) + if err != nil { + return nil, services.HandleGetError("GatewayProfile", "id", id, err) + } + return gatewayProfile, nil +} + +func (s *sqlGatewayProfileService) Create(ctx context.Context, gatewayProfile *GatewayProfile) (*GatewayProfile, *errors.ServiceError) { + if svcErr := validateProfileFields(gatewayProfile); svcErr != nil { + return nil, svcErr + } + + gatewayProfile, err := s.gatewayProfileDao.Create(ctx, gatewayProfile) + if err != nil { + return nil, services.HandleCreateError("GatewayProfile", err) + } + + _, evErr := s.events.Create(ctx, &api.Event{ + Source: "GatewayProfiles", + SourceID: gatewayProfile.ID, + EventType: api.CreateEventType, + }) + if evErr != nil { + return nil, services.HandleCreateError("GatewayProfile", evErr) + } + + return gatewayProfile, nil +} + +func (s *sqlGatewayProfileService) Replace(ctx context.Context, gatewayProfile *GatewayProfile) (*GatewayProfile, *errors.ServiceError) { + if svcErr := validateProfileFields(gatewayProfile); svcErr != nil { + return nil, svcErr + } + + lockOwnerID, err := s.lockFactory.NewAdvisoryLock(ctx, gatewayProfile.ID, gatewayProfilesLockType) + if err != nil { + return nil, errors.DatabaseAdvisoryLock(err) + } + defer s.lockFactory.Unlock(ctx, lockOwnerID) + + gatewayProfile, err = s.gatewayProfileDao.Replace(ctx, gatewayProfile) + if err != nil { + return nil, services.HandleUpdateError("GatewayProfile", err) + } + + _, evErr := s.events.Create(ctx, &api.Event{ + Source: "GatewayProfiles", + SourceID: gatewayProfile.ID, + EventType: api.UpdateEventType, + }) + if evErr != nil { + return nil, services.HandleUpdateError("GatewayProfile", evErr) + } + + return gatewayProfile, nil +} + +func (s *sqlGatewayProfileService) Delete(ctx context.Context, id string) *errors.ServiceError { + if _, svcErr := s.Get(ctx, id); svcErr != nil { + return svcErr + } + + // Deletion protection: a profile referenced by a cluster default or by any + // gateway must not be deleted, or those referrers would carry a dangling + // profile_id that blocks gateway provisioning. + clusterRef, refErr := s.gatewayProfileDao.ExistsByClusterProfileID(ctx, id) + if refErr != nil { + return errors.GeneralError("check cluster references: %s", refErr) + } + if clusterRef { + return errors.Conflict("GatewayProfile %s is the default profile for one or more clusters and cannot be deleted", id) + } + + gatewayRef, refErr := s.gatewayProfileDao.ExistsByGatewayProfileID(ctx, id) + if refErr != nil { + return errors.GeneralError("check gateway references: %s", refErr) + } + if gatewayRef { + return errors.Conflict("GatewayProfile %s is referenced by one or more gateways and cannot be deleted", id) + } + + if err := s.gatewayProfileDao.Delete(ctx, id); err != nil { + return services.HandleDeleteError("GatewayProfile", errors.GeneralError("Unable to delete gatewayProfile: %s", err)) + } + + _, evErr := s.events.Create(ctx, &api.Event{ + Source: "GatewayProfiles", + SourceID: id, + EventType: api.DeleteEventType, + }) + if evErr != nil { + return services.HandleDeleteError("GatewayProfile", evErr) + } + + return nil +} + +func (s *sqlGatewayProfileService) FindByIDs(ctx context.Context, ids []string) (GatewayProfileList, *errors.ServiceError) { + gatewayProfiles, err := s.gatewayProfileDao.FindByIDs(ctx, ids) + if err != nil { + return nil, errors.GeneralError("Unable to get all gatewayProfiles: %s", err) + } + return gatewayProfiles, nil +} + +func (s *sqlGatewayProfileService) All(ctx context.Context) (GatewayProfileList, *errors.ServiceError) { + gatewayProfiles, err := s.gatewayProfileDao.All(ctx) + if err != nil { + return nil, errors.GeneralError("Unable to get all gatewayProfiles: %s", err) + } + return gatewayProfiles, nil +} diff --git a/components/api-server/plugins/gatewayProfiles/service_test.go b/components/api-server/plugins/gatewayProfiles/service_test.go new file mode 100644 index 00000000..af3775c5 --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/service_test.go @@ -0,0 +1,277 @@ +package gatewayProfiles + +import ( + "context" + "net/http" + "testing" + "time" + + "github.com/openshift-online/rh-trex-ai/pkg/api" + "github.com/openshift-online/rh-trex-ai/pkg/db" + "github.com/openshift-online/rh-trex-ai/pkg/errors" +) + +// noopEventService satisfies services.EventService for unit tests that do not +// exercise event delivery. It counts Create calls so tests can assert the +// service emitted the expected lifecycle event. +type noopEventService struct{ createCount int } + +func (n *noopEventService) Get(_ context.Context, _ string) (*api.Event, *errors.ServiceError) { + return nil, nil +} +func (n *noopEventService) Create(_ context.Context, ev *api.Event) (*api.Event, *errors.ServiceError) { + n.createCount++ + return ev, nil +} +func (n *noopEventService) Replace(_ context.Context, ev *api.Event) (*api.Event, *errors.ServiceError) { + return ev, nil +} +func (n *noopEventService) Delete(_ context.Context, _ string) *errors.ServiceError { return nil } +func (n *noopEventService) All(_ context.Context) (api.EventList, *errors.ServiceError) { + return nil, nil +} +func (n *noopEventService) FindByIDs(_ context.Context, _ []string) (api.EventList, *errors.ServiceError) { + return nil, nil +} +func (n *noopEventService) FindUnreconciled(_ context.Context, _ time.Duration) (api.EventList, *errors.ServiceError) { + return nil, nil +} +func (n *noopEventService) FindBySourceAndType(_ context.Context, _ string, _ api.EventType) (api.EventList, *errors.ServiceError) { + return nil, nil +} + +// stubLockFactory satisfies db.LockFactory without a database so the Replace +// advisory-lock path can be exercised in a unit test. +type stubLockFactory struct{} + +func (stubLockFactory) NewAdvisoryLock(_ context.Context, _ string, _ db.LockType) (string, error) { + return "owner", nil +} +func (stubLockFactory) NewNonBlockingLock(_ context.Context, _ string, _ db.LockType) (string, bool, error) { + return "owner", true, nil +} +func (stubLockFactory) Unlock(_ context.Context, _ string) {} + +// testProfileDao wraps the shipped mock_dao.go so unit tests reuse its +// Get/Create/All behaviour, while supplying working Delete/Replace +// implementations (the shipped mock returns NotImplemented for those) and +// configurable reference facts for deletion-protection tests. +type testProfileDao struct { + *gatewayProfileDaoMock + clusterReferenced bool + gatewayReferenced bool +} + +func newTestProfileDao() *testProfileDao { + return &testProfileDao{gatewayProfileDaoMock: NewMockGatewayProfileDao()} +} + +func (d *testProfileDao) Delete(_ context.Context, id string) error { + kept := GatewayProfileList{} + for _, p := range d.gatewayProfiles { + if p.ID != id { + kept = append(kept, p) + } + } + d.gatewayProfiles = kept + return nil +} + +func (d *testProfileDao) Replace(_ context.Context, p *GatewayProfile) (*GatewayProfile, error) { + for i, existing := range d.gatewayProfiles { + if existing.ID == p.ID { + d.gatewayProfiles[i] = p + return p, nil + } + } + d.gatewayProfiles = append(d.gatewayProfiles, p) + return p, nil +} + +func (d *testProfileDao) ExistsByClusterProfileID(_ context.Context, _ string) (bool, error) { + return d.clusterReferenced, nil +} + +func (d *testProfileDao) ExistsByGatewayProfileID(_ context.Context, _ string) (bool, error) { + return d.gatewayReferenced, nil +} + +func newTestService(dao GatewayProfileDao, events *noopEventService) GatewayProfileService { + return NewGatewayProfileService(stubLockFactory{}, dao, events) +} + +// seed inserts a profile with a fixed ID directly through the mock so lookups +// (which match on ID) resolve; the mock does not run the BeforeCreate hook. +func seed(dao *testProfileDao, id, name string) *GatewayProfile { + p := &GatewayProfile{Name: name} + p.ID = id + dao.gatewayProfiles = append(dao.gatewayProfiles, p) + return p +} + +func TestGatewayProfileService_Create(t *testing.T) { + t.Run("valid profile is persisted and emits a create event", func(t *testing.T) { + dao := newTestProfileDao() + events := &noopEventService{} + svc := newTestService(dao, events) + + profile := &GatewayProfile{Name: "small", CpuRequestTotal: strPtr("2"), MemoryLimitTotal: strPtr("8Gi")} + profile.ID = "p-create" + + created, svcErr := svc.Create(context.Background(), profile) + if svcErr != nil { + t.Fatalf("Create() unexpected error: %v", svcErr) + } + if created.Name != "small" { + t.Fatalf("Create() name = %q, want %q", created.Name, "small") + } + if events.createCount != 1 { + t.Fatalf("Create() emitted %d events, want 1", events.createCount) + } + + got, getErr := svc.Get(context.Background(), "p-create") + if getErr != nil { + t.Fatalf("Get() after Create() unexpected error: %v", getErr) + } + if got.Name != "small" { + t.Fatalf("Get() name = %q, want %q", got.Name, "small") + } + }) + + t.Run("invalid quantity is rejected before persistence", func(t *testing.T) { + dao := newTestProfileDao() + events := &noopEventService{} + svc := newTestService(dao, events) + + _, svcErr := svc.Create(context.Background(), &GatewayProfile{Name: "bad", MemoryLimitTotal: strPtr("not-a-quantity")}) + if svcErr == nil { + t.Fatal("Create() = nil error, want HTTP 400 for invalid quantity") + } + if svcErr.HttpCode != http.StatusBadRequest { + t.Fatalf("Create() HttpCode = %d, want 400", svcErr.HttpCode) + } + all, _ := svc.All(context.Background()) + if len(all) != 0 { + t.Fatalf("invalid profile persisted; All() len = %d, want 0", len(all)) + } + if events.createCount != 0 { + t.Fatalf("invalid profile emitted %d events, want 0", events.createCount) + } + }) +} + +func TestGatewayProfileService_Get_NotFound(t *testing.T) { + svc := newTestService(newTestProfileDao(), &noopEventService{}) + + _, svcErr := svc.Get(context.Background(), "missing") + if svcErr == nil { + t.Fatal("Get() = nil error, want not-found") + } + if svcErr.HttpCode != http.StatusNotFound { + t.Fatalf("Get() HttpCode = %d, want 404", svcErr.HttpCode) + } +} + +func TestGatewayProfileService_Replace(t *testing.T) { + dao := newTestProfileDao() + events := &noopEventService{} + svc := newTestService(dao, events) + + seed(dao, "p-1", "before") + + updated := &GatewayProfile{Name: "after", CpuRequestTotal: strPtr("4")} + updated.ID = "p-1" + + got, svcErr := svc.Replace(context.Background(), updated) + if svcErr != nil { + t.Fatalf("Replace() unexpected error: %v", svcErr) + } + if got.Name != "after" { + t.Fatalf("Replace() name = %q, want %q", got.Name, "after") + } + if events.createCount != 1 { + t.Fatalf("Replace() emitted %d events, want 1", events.createCount) + } + + roundTrip, _ := svc.Get(context.Background(), "p-1") + if roundTrip.Name != "after" { + t.Fatalf("Get() after Replace() name = %q, want %q", roundTrip.Name, "after") + } +} + +func TestGatewayProfileService_Replace_ValidatesFields(t *testing.T) { + dao := newTestProfileDao() + svc := newTestService(dao, &noopEventService{}) + seed(dao, "p-1", "before") + + bad := &GatewayProfile{Name: "before", MemoryLimitTotal: strPtr("2GB")} + bad.ID = "p-1" + + _, svcErr := svc.Replace(context.Background(), bad) + if svcErr == nil { + t.Fatal("Replace() = nil error, want HTTP 400 for invalid quantity") + } + if svcErr.HttpCode != http.StatusBadRequest { + t.Fatalf("Replace() HttpCode = %d, want 400", svcErr.HttpCode) + } +} + +func TestGatewayProfileService_Delete(t *testing.T) { + t.Run("unreferenced profile is deleted", func(t *testing.T) { + dao := newTestProfileDao() + events := &noopEventService{} + svc := newTestService(dao, events) + seed(dao, "p-free", "unused") + + if svcErr := svc.Delete(context.Background(), "p-free"); svcErr != nil { + t.Fatalf("Delete() unexpected error: %v", svcErr) + } + if events.createCount != 1 { + t.Fatalf("Delete() emitted %d events, want 1", events.createCount) + } + if _, getErr := svc.Get(context.Background(), "p-free"); getErr == nil { + t.Fatal("Get() after Delete() succeeded, want not-found") + } + }) + + t.Run("missing profile returns not-found", func(t *testing.T) { + svc := newTestService(newTestProfileDao(), &noopEventService{}) + svcErr := svc.Delete(context.Background(), "nope") + if svcErr == nil { + t.Fatal("Delete() = nil error, want not-found") + } + if svcErr.HttpCode != http.StatusNotFound { + t.Fatalf("Delete() HttpCode = %d, want 404", svcErr.HttpCode) + } + }) + + t.Run("profile referenced by a cluster default is protected", func(t *testing.T) { + dao := newTestProfileDao() + dao.clusterReferenced = true + svc := newTestService(dao, &noopEventService{}) + seed(dao, "p-ref", "in-use") + + svcErr := svc.Delete(context.Background(), "p-ref") + if svcErr == nil { + t.Fatal("Delete() = nil error, want HTTP 409 when referenced by a cluster") + } + if svcErr.HttpCode != http.StatusConflict { + t.Fatalf("Delete() HttpCode = %d, want 409", svcErr.HttpCode) + } + }) + + t.Run("profile referenced by a gateway is protected", func(t *testing.T) { + dao := newTestProfileDao() + dao.gatewayReferenced = true + svc := newTestService(dao, &noopEventService{}) + seed(dao, "p-ref", "in-use") + + svcErr := svc.Delete(context.Background(), "p-ref") + if svcErr == nil { + t.Fatal("Delete() = nil error, want HTTP 409 when referenced by a gateway") + } + if svcErr.HttpCode != http.StatusConflict { + t.Fatalf("Delete() HttpCode = %d, want 409", svcErr.HttpCode) + } + }) +} diff --git a/components/api-server/plugins/gatewayProfiles/validate.go b/components/api-server/plugins/gatewayProfiles/validate.go new file mode 100644 index 00000000..7469ea6b --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/validate.go @@ -0,0 +1,99 @@ +package gatewayProfiles + +import ( + "k8s.io/apimachinery/pkg/api/resource" + + "github.com/openshift-online/rh-trex-ai/pkg/errors" +) + +// validateQuantity validates a single optional Kubernetes resource quantity +// field. Empty/unset is valid ("not set"). A non-empty value must parse as a +// valid, non-negative quantity; otherwise a validation error is returned. +func validateQuantity(field string, value *string) *errors.ServiceError { + if value == nil || *value == "" { + return nil + } + q, err := resource.ParseQuantity(*value) + if err != nil { + return errors.Validation("invalid quantity for %s: %q is not a valid Kubernetes resource quantity", field, *value) + } + if q.Sign() < 0 { + return errors.Validation("invalid quantity for %s: %q must not be negative", field, *value) + } + return nil +} + +// validateCount validates an optional non-negative count field. Nil or zero is +// valid ("not set"); a negative value is rejected. +func validateCount(field string, value *int32) *errors.ServiceError { + if value == nil { + return nil + } + if *value < 0 { + return errors.Validation("invalid count for %s: %d must not be negative", field, *value) + } + return nil +} + +// validateRequestNotExceedsLimit checks that a request quantity does not exceed +// its corresponding limit when both are set. Skipped when either is unset. +func validateRequestNotExceedsLimit(requestField string, requestValue *string, limitField string, limitValue *string) *errors.ServiceError { + if requestValue == nil || *requestValue == "" || limitValue == nil || *limitValue == "" { + return nil + } + req, _ := resource.ParseQuantity(*requestValue) + lim, _ := resource.ParseQuantity(*limitValue) + if req.Cmp(lim) > 0 { + return errors.Validation("%s (%s) must not exceed %s (%s)", requestField, *requestValue, limitField, *limitValue) + } + return nil +} + +// validateProfileFields validates every quantity and count field on a +// GatewayProfile at the API boundary so invalid values are rejected with HTTP +// 400 rather than persisted and later failing control-plane reconciliation. +func validateProfileFields(p *GatewayProfile) *errors.ServiceError { + quantities := []struct { + field string + value *string + }{ + {"cpu_request_total", p.CpuRequestTotal}, + {"cpu_limit_total", p.CpuLimitTotal}, + {"memory_request_total", p.MemoryRequestTotal}, + {"memory_limit_total", p.MemoryLimitTotal}, + {"ephemeral_storage_total", p.EphemeralStorageTotal}, + {"container_cpu_request_default", p.ContainerCpuRequestDefault}, + {"container_cpu_limit_max", p.ContainerCpuLimitMax}, + {"container_memory_request_default", p.ContainerMemoryRequestDefault}, + {"container_memory_limit_max", p.ContainerMemoryLimitMax}, + } + for _, q := range quantities { + if svcErr := validateQuantity(q.field, q.value); svcErr != nil { + return svcErr + } + } + if svcErr := validateCount("pod_count", p.PodCount); svcErr != nil { + return svcErr + } + if svcErr := validateCount("pvc_count", p.PvcCount); svcErr != nil { + return svcErr + } + + crossChecks := []struct { + reqField string + reqValue *string + limField string + limValue *string + }{ + {"cpu_request_total", p.CpuRequestTotal, "cpu_limit_total", p.CpuLimitTotal}, + {"memory_request_total", p.MemoryRequestTotal, "memory_limit_total", p.MemoryLimitTotal}, + {"container_cpu_request_default", p.ContainerCpuRequestDefault, "container_cpu_limit_max", p.ContainerCpuLimitMax}, + {"container_memory_request_default", p.ContainerMemoryRequestDefault, "container_memory_limit_max", p.ContainerMemoryLimitMax}, + } + for _, c := range crossChecks { + if svcErr := validateRequestNotExceedsLimit(c.reqField, c.reqValue, c.limField, c.limValue); svcErr != nil { + return svcErr + } + } + return nil +} diff --git a/components/api-server/plugins/gatewayProfiles/validate_test.go b/components/api-server/plugins/gatewayProfiles/validate_test.go new file mode 100644 index 00000000..0735d1ea --- /dev/null +++ b/components/api-server/plugins/gatewayProfiles/validate_test.go @@ -0,0 +1,159 @@ +package gatewayProfiles + +import ( + "net/http" + "testing" +) + +func strPtr(s string) *string { return &s } +func int32Ptr(i int32) *int32 { return &i } + +// TestValidateProfileFields exercises the real k8s.io/apimachinery quantity +// parser wired into validate.go. Empty/unset values are "not set" and always +// pass; non-empty values must parse as valid, non-negative Kubernetes resource +// quantities; counts must be non-negative. +func TestValidateProfileFields(t *testing.T) { + t.Run("nil and empty fields are accepted", func(t *testing.T) { + if svcErr := validateProfileFields(&GatewayProfile{Name: "empty"}); svcErr != nil { + t.Fatalf("expected no error for empty profile, got %v", svcErr) + } + if svcErr := validateProfileFields(&GatewayProfile{CpuRequestTotal: strPtr("")}); svcErr != nil { + t.Fatalf("expected no error for empty-string quantity, got %v", svcErr) + } + }) + + t.Run("valid quantities and counts are accepted", func(t *testing.T) { + p := &GatewayProfile{ + CpuRequestTotal: strPtr("2"), + CpuLimitTotal: strPtr("4"), + MemoryRequestTotal: strPtr("4Gi"), + MemoryLimitTotal: strPtr("8Gi"), + EphemeralStorageTotal: strPtr("10Gi"), + ContainerCpuRequestDefault: strPtr("100m"), + ContainerCpuLimitMax: strPtr("1500m"), + ContainerMemoryRequestDefault: strPtr("128Mi"), + ContainerMemoryLimitMax: strPtr("1G"), + PodCount: int32Ptr(10), + PvcCount: int32Ptr(0), // zero is a valid "not set" count + } + if svcErr := validateProfileFields(p); svcErr != nil { + t.Fatalf("expected valid profile to pass, got %v", svcErr) + } + }) + + badQuantities := map[string]string{ + "garbage": "tow", + "bad unit": "2GB", + "trailing text": "5Gib", + "space": "5 Gi", + "double suffix": "5GiGi", + } + for name, val := range badQuantities { + val := val + t.Run("rejects unparseable quantity: "+name, func(t *testing.T) { + svcErr := validateProfileFields(&GatewayProfile{MemoryLimitTotal: strPtr(val)}) + if svcErr == nil { + t.Fatalf("expected validation error for %q, got nil", val) + } + if svcErr.HttpCode != http.StatusBadRequest { + t.Fatalf("expected HTTP 400 for %q, got %d", val, svcErr.HttpCode) + } + }) + } + + t.Run("rejects a negative quantity", func(t *testing.T) { + svcErr := validateProfileFields(&GatewayProfile{CpuRequestTotal: strPtr("-1")}) + if svcErr == nil { + t.Fatal("expected validation error for negative quantity, got nil") + } + if svcErr.HttpCode != http.StatusBadRequest { + t.Fatalf("expected HTTP 400, got %d", svcErr.HttpCode) + } + }) + + t.Run("rejects negative counts", func(t *testing.T) { + cases := []struct { + name string + profile *GatewayProfile + }{ + {"pod_count", &GatewayProfile{PodCount: int32Ptr(-1)}}, + {"pvc_count", &GatewayProfile{PvcCount: int32Ptr(-5)}}, + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + svcErr := validateProfileFields(tc.profile) + if svcErr == nil { + t.Fatalf("expected error for negative %s, got nil", tc.name) + } + if svcErr.HttpCode != http.StatusBadRequest { + t.Fatalf("expected HTTP 400 for negative %s, got %d", tc.name, svcErr.HttpCode) + } + }) + } + }) + + t.Run("accepts nil and zero counts", func(t *testing.T) { + if svcErr := validateProfileFields(&GatewayProfile{}); svcErr != nil { + t.Fatalf("expected nil counts to pass, got %v", svcErr) + } + if svcErr := validateProfileFields(&GatewayProfile{PodCount: int32Ptr(0), PvcCount: int32Ptr(0)}); svcErr != nil { + t.Fatalf("expected zero counts to pass, got %v", svcErr) + } + }) + + t.Run("cross-field: request exceeding limit is rejected", func(t *testing.T) { + cases := []struct { + name string + profile *GatewayProfile + }{ + {"cpu_request_total > cpu_limit_total", &GatewayProfile{ + CpuRequestTotal: strPtr("8"), CpuLimitTotal: strPtr("4"), + }}, + {"memory_request_total > memory_limit_total", &GatewayProfile{ + MemoryRequestTotal: strPtr("16Gi"), MemoryLimitTotal: strPtr("8Gi"), + }}, + {"container_cpu_request_default > container_cpu_limit_max", &GatewayProfile{ + ContainerCpuRequestDefault: strPtr("2"), ContainerCpuLimitMax: strPtr("500m"), + }}, + {"container_memory_request_default > container_memory_limit_max", &GatewayProfile{ + ContainerMemoryRequestDefault: strPtr("512Mi"), ContainerMemoryLimitMax: strPtr("256Mi"), + }}, + } + for _, tc := range cases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + svcErr := validateProfileFields(tc.profile) + if svcErr == nil { + t.Fatalf("expected validation error for %s, got nil", tc.name) + } + if svcErr.HttpCode != http.StatusBadRequest { + t.Fatalf("expected HTTP 400 for %s, got %d", tc.name, svcErr.HttpCode) + } + }) + } + }) + + t.Run("cross-field: request equal to limit is accepted", func(t *testing.T) { + p := &GatewayProfile{ + CpuRequestTotal: strPtr("4"), + CpuLimitTotal: strPtr("4"), + MemoryRequestTotal: strPtr("8Gi"), + MemoryLimitTotal: strPtr("8Gi"), + ContainerCpuRequestDefault: strPtr("500m"), + ContainerCpuLimitMax: strPtr("500m"), + ContainerMemoryRequestDefault: strPtr("256Mi"), + ContainerMemoryLimitMax: strPtr("256Mi"), + } + if svcErr := validateProfileFields(p); svcErr != nil { + t.Fatalf("expected request==limit to pass, got %v", svcErr) + } + }) + + t.Run("cross-field: unset limit skips request check", func(t *testing.T) { + // Only cpu_request_total is set; cpu_limit_total is nil → no cross-check. + if svcErr := validateProfileFields(&GatewayProfile{CpuRequestTotal: strPtr("100")}); svcErr != nil { + t.Fatalf("expected unset limit to skip cross-check, got %v", svcErr) + } + }) +} diff --git a/components/api-server/plugins/gateways/grpc_handler.go b/components/api-server/plugins/gateways/grpc_handler.go index 5cd04847..24b1bf88 100644 --- a/components/api-server/plugins/gateways/grpc_handler.go +++ b/components/api-server/plugins/gateways/grpc_handler.go @@ -74,6 +74,9 @@ func (h *gatewayGRPCHandler) CreateGateway(ctx context.Context, req *pb.CreateGa Oidc: req.Oidc, Route: req.Route, } + if req.ProfileId != nil { + gateway.ProfileId = *req.ProfileId + } result, svcErr := h.service.Create(ctx, gateway) if svcErr != nil { return nil, grpcutil.ServiceErrorToGRPC(svcErr) @@ -141,6 +144,16 @@ func (h *gatewayGRPCHandler) UpdateGateway(ctx context.Context, req *pb.UpdateGa } // database_id is server-owned placement state. Ignore values supplied by // callers; gateway creation business logic is the only assignment path. + if req.ProfileId != nil && *req.ProfileId != "" { + exists, existsErr := h.service.ProfileExists(ctx, *req.ProfileId) + if existsErr != nil { + return nil, grpcutil.ServiceErrorToGRPC(existsErr) + } + if !exists { + return nil, status.Errorf(codes.NotFound, "gateway profile %s does not exist", *req.ProfileId) + } + gateway.ProfileId = *req.ProfileId + } if req.ExternalDns != nil { gateway.ExternalDns = req.ExternalDns } diff --git a/components/api-server/plugins/gateways/grpc_presenter.go b/components/api-server/plugins/gateways/grpc_presenter.go index a4b685b4..c254d7fa 100644 --- a/components/api-server/plugins/gateways/grpc_presenter.go +++ b/components/api-server/plugins/gateways/grpc_presenter.go @@ -42,6 +42,11 @@ func gatewayToProto(d *Gateway) *pb.Gateway { }(), } + if d.ProfileId != "" { + pid := d.ProfileId + gw.ProfileId = &pid + } + if d.ServerDnsNames != nil { var names []string if err := json.Unmarshal([]byte(*d.ServerDnsNames), &names); err == nil { diff --git a/components/api-server/plugins/gateways/handler.go b/components/api-server/plugins/gateways/handler.go index b724bd9b..df28028e 100644 --- a/components/api-server/plugins/gateways/handler.go +++ b/components/api-server/plugins/gateways/handler.go @@ -96,6 +96,16 @@ func (h gatewayHandler) Patch(w http.ResponseWriter, r *http.Request) { found.Name = *patch.Name } if patch.ClusterId != nil { + if *patch.ClusterId == "" { + return nil, errors.Validation("cluster_id cannot be removed from a gateway") + } + clusterExists, clusterExistsErr := h.gateway.ClusterExists(ctx, *patch.ClusterId) + if clusterExistsErr != nil { + return nil, clusterExistsErr + } + if !clusterExists { + return nil, errors.Validation("cluster %s does not exist", *patch.ClusterId) + } found.ClusterId = *patch.ClusterId } if patch.ReleaseId != nil { @@ -103,6 +113,21 @@ func (h gatewayHandler) Patch(w http.ResponseWriter, r *http.Request) { } // database_id is server-owned placement state. Ignore any value supplied // through the public API; only gateway creation business logic assigns it. + if patch.ProfileId != nil { + // A gateway must always have a profile: reject clearing it. Reassignment + // to a different existing profile is allowed and re-triggers reconcile. + if *patch.ProfileId == "" { + return nil, errors.Validation("profile_id cannot be removed from a gateway; reassign it to a different profile instead") + } + exists, existsErr := h.gateway.ProfileExists(ctx, *patch.ProfileId) + if existsErr != nil { + return nil, existsErr + } + if !exists { + return nil, errors.Validation("gateway profile %s does not exist", *patch.ProfileId) + } + found.ProfileId = *patch.ProfileId + } if patch.ExternalDns != nil { found.ExternalDns = patch.ExternalDns } diff --git a/components/api-server/plugins/gateways/integration_profile_test.go b/components/api-server/plugins/gateways/integration_profile_test.go new file mode 100644 index 00000000..095d18ef --- /dev/null +++ b/components/api-server/plugins/gateways/integration_profile_test.go @@ -0,0 +1,101 @@ +package gateways_test + +import ( + "context" + "net/http" + "testing" + + . "github.com/onsi/gomega" + + "github.com/openshift-online/hypershell/components/api-server/pkg/api/openapi" + "github.com/openshift-online/hypershell/components/api-server/test" +) + +// createGatewayProfile creates a GatewayProfile through the OpenAPI client and +// returns its ID for use in gateway profile-assignment tests. +func createGatewayProfile(ctx context.Context, client *openapi.APIClient, name string) string { + profile, resp, err := client.DefaultAPI.CreateGatewayProfile(ctx). + GatewayProfile(openapi.GatewayProfile{Name: name, CpuRequestTotal: openapi.PtrString("1")}). + Execute() + Expect(err).NotTo(HaveOccurred(), "Error creating gateway profile: %v", err) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + return *profile.Id +} + +// TestGatewayPostAssignsProfile covers profile assignment on gateway creation: +// a client-supplied, existing profile_id is persisted on the gateway. +func TestGatewayPostAssignsProfile(t *testing.T) { + h, client := test.RegisterIntegration(t) + + account := h.NewRandAccount() + ctx := h.NewAuthenticatedContext(account) + + profileID := createGatewayProfile(ctx, client, "assign-on-create") + + gatewayInput := openapi.GatewayCreateRequest{ + Name: "gw-with-profile", + ProfileId: openapi.PtrString(profileID), + } + gateway, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(gatewayInput).Execute() + Expect(err).NotTo(HaveOccurred(), "Error creating gateway with profile: %v", err) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + Expect(gateway.GetProfileId()).To(Equal(profileID)) +} + +// TestGatewayPostRejectsNonexistentProfile covers create-time profile +// validation: a profile_id that does not exist is rejected with HTTP 400. +func TestGatewayPostRejectsNonexistentProfile(t *testing.T) { + h, client := test.RegisterIntegration(t) + + account := h.NewRandAccount() + ctx := h.NewAuthenticatedContext(account) + + gatewayInput := openapi.GatewayCreateRequest{ + Name: "gw-bad-profile", + ProfileId: openapi.PtrString("does-not-exist"), + } + _, resp, err := client.DefaultAPI.CreateGateway(ctx).GatewayCreateRequest(gatewayInput).Execute() + Expect(err).To(HaveOccurred(), "Expected create with nonexistent profile to be rejected") + Expect(resp.StatusCode).To(Equal(http.StatusBadRequest)) +} + +// TestGatewayPatchReassignsProfile covers profile reassignment via PATCH: +// reassigning to another existing profile succeeds; clearing the profile is +// rejected; and reassigning to a nonexistent profile is rejected. +func TestGatewayPatchReassignsProfile(t *testing.T) { + h, client := test.RegisterIntegration(t) + + account := h.NewRandAccount() + ctx := h.NewAuthenticatedContext(account) + + profileA := createGatewayProfile(ctx, client, "reassign-a") + profileB := createGatewayProfile(ctx, client, "reassign-b") + + created, resp, err := client.DefaultAPI.CreateGateway(ctx). + GatewayCreateRequest(openapi.GatewayCreateRequest{Name: "gw-reassign", ProfileId: openapi.PtrString(profileA)}). + Execute() + Expect(err).NotTo(HaveOccurred(), "Error creating gateway: %v", err) + Expect(resp.StatusCode).To(Equal(http.StatusCreated)) + + // Reassign to another existing profile succeeds. + patched, resp, err := client.DefaultAPI.UpdateGateway(ctx, *created.Id). + GatewayPatchRequest(openapi.GatewayPatchRequest{ProfileId: openapi.PtrString(profileB)}). + Execute() + Expect(err).NotTo(HaveOccurred(), "Error reassigning gateway profile: %v", err) + Expect(resp.StatusCode).To(Equal(http.StatusOK)) + Expect(patched.GetProfileId()).To(Equal(profileB)) + + // Clearing the profile is forbidden. + _, resp, err = client.DefaultAPI.UpdateGateway(ctx, *created.Id). + GatewayPatchRequest(openapi.GatewayPatchRequest{ProfileId: openapi.PtrString("")}). + Execute() + Expect(err).To(HaveOccurred(), "Expected clearing profile_id to be rejected") + Expect(resp.StatusCode).To(Equal(http.StatusBadRequest)) + + // Reassigning to a nonexistent profile is rejected. + _, resp, err = client.DefaultAPI.UpdateGateway(ctx, *created.Id). + GatewayPatchRequest(openapi.GatewayPatchRequest{ProfileId: openapi.PtrString("does-not-exist")}). + Execute() + Expect(err).To(HaveOccurred(), "Expected reassignment to a nonexistent profile to be rejected") + Expect(resp.StatusCode).To(Equal(http.StatusBadRequest)) +} diff --git a/components/api-server/plugins/gateways/migration.go b/components/api-server/plugins/gateways/migration.go index 8262615f..921d0091 100644 --- a/components/api-server/plugins/gateways/migration.go +++ b/components/api-server/plugins/gateways/migration.go @@ -115,6 +115,23 @@ func migrationAddActiveSandboxCount() *gormigrate.Migration { } } +func migrationAddProfileID() *gormigrate.Migration { + type Gateway struct { + db.Model + ProfileId string + } + + return &gormigrate.Migration{ + ID: "2026082800000002", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate(&Gateway{}) + }, + Rollback: func(tx *gorm.DB) error { + return tx.Migrator().DropColumn(&Gateway{}, "profile_id") + }, + } +} + func migrationDropDatabaseConfig() *gormigrate.Migration { type Gateway struct{ db.Model } diff --git a/components/api-server/plugins/gateways/model.go b/components/api-server/plugins/gateways/model.go index 276b78f5..b76fa5f4 100644 --- a/components/api-server/plugins/gateways/model.go +++ b/components/api-server/plugins/gateways/model.go @@ -17,6 +17,7 @@ type Gateway struct { ClusterId string `json:"cluster_id"` ReleaseId string `json:"release_id"` DatabaseId string `json:"database_id"` + ProfileId string `json:"profile_id"` Namespace string `json:"namespace"` ExternalDns *string `json:"external_dns"` TlsMode *string `json:"tls_mode"` @@ -61,6 +62,7 @@ type GatewayPatchRequest struct { ClusterId *string `json:"cluster_id,omitempty"` ReleaseId *string `json:"release_id,omitempty"` DatabaseId *string `json:"database_id,omitempty"` + ProfileId *string `json:"profile_id,omitempty"` ExternalDns *string `json:"external_dns,omitempty"` TlsMode *string `json:"tls_mode,omitempty"` ServiceType *string `json:"service_type,omitempty"` diff --git a/components/api-server/plugins/gateways/plugin.go b/components/api-server/plugins/gateways/plugin.go index a62b8d41..e97ef7cf 100644 --- a/components/api-server/plugins/gateways/plugin.go +++ b/components/api-server/plugins/gateways/plugin.go @@ -11,6 +11,8 @@ import ( pb "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1" "github.com/openshift-online/hypershell/components/api-server/pkg/rbac" + "github.com/openshift-online/hypershell/components/api-server/plugins/gatewayProfiles" + "github.com/openshift-online/hypershell/components/api-server/plugins/managedClusters" "github.com/openshift-online/hypershell/components/api-server/plugins/managedDatabases" "github.com/openshift-online/hypershell/components/api-server/plugins/roleBindings" "github.com/openshift-online/rh-trex-ai/pkg/api" @@ -62,6 +64,56 @@ func (a *dbCreatorAdapter) CreateForGateway(ctx context.Context, gatewayName str return d.ID, nil } +// profileResolverAdapter satisfies ProfileResolver by delegating to the +// managedClusters and gatewayProfiles services, keeping the gateway service +// decoupled from those packages' concrete types. +type profileResolverAdapter struct { + clusters managedClusters.ManagedClusterService + profiles gatewayProfiles.GatewayProfileService +} + +func (a *profileResolverAdapter) ClusterDefaultProfileID(ctx context.Context, clusterID string) (string, error) { + if a.clusters == nil || clusterID == "" { + return "", nil + } + c, svcErr := a.clusters.Get(ctx, clusterID) + if svcErr != nil { + return "", svcErr + } + if c.ProfileId != nil { + return *c.ProfileId, nil + } + return "", nil +} + +func (a *profileResolverAdapter) ProfileExists(ctx context.Context, profileID string) (bool, error) { + if a.profiles == nil || profileID == "" { + return false, nil + } + _, svcErr := a.profiles.Get(ctx, profileID) + if svcErr != nil { + if svcErr.Is404() { + return false, nil + } + return false, svcErr + } + return true, nil +} + +func (a *profileResolverAdapter) ClusterExists(ctx context.Context, clusterID string) (bool, error) { + if a.clusters == nil || clusterID == "" { + return false, nil + } + _, svcErr := a.clusters.Get(ctx, clusterID) + if svcErr != nil { + if svcErr.Is404() { + return false, nil + } + return false, svcErr + } + return true, nil +} + func NewServiceLocator(env *environments.Env) ServiceLocator { dao := NewGatewayDao(&env.Database.SessionFactory) RegisterGatewayMetrics(dao) @@ -87,11 +139,17 @@ func NewServiceLocator(env *environments.Env) ServiceLocator { } } + profiles := &profileResolverAdapter{ + clusters: managedClusters.Service(&env.Services), + profiles: gatewayProfiles.Service(&env.Services), + } + return NewGatewayService( db.NewAdvisoryLockFactory(env.Database.SessionFactory), dao, events.Service(&env.Services), placement, + profiles, ) }, list: newGatewayListService(&env.Database.SessionFactory), @@ -191,6 +249,7 @@ func init() { db.RegisterMigration(migrationAddCredentialDriver()) db.RegisterMigration(migrationAddConsoleAddress()) db.RegisterMigration(migrationAddActiveSandboxCount()) + db.RegisterMigration(migrationAddProfileID()) db.RegisterMigration(migrationDropDatabaseConfig()) db.RegisterMigration(migrationDropFleetId()) db.RegisterMigration(migrationDropFleetsTable()) diff --git a/components/api-server/plugins/gateways/presenter.go b/components/api-server/plugins/gateways/presenter.go index 5cbec7d8..5895436f 100644 --- a/components/api-server/plugins/gateways/presenter.go +++ b/components/api-server/plugins/gateways/presenter.go @@ -14,6 +14,9 @@ func ConvertGateway(gateway openapi.GatewayCreateRequest) *Gateway { c.ReleaseId = gateway.ReleaseId // database_id is assigned by the configured server-side placement strategy. // Deliberately ignore any value supplied by the API client. + if gateway.ProfileId != nil { + c.ProfileId = *gateway.ProfileId + } c.ExternalDns = gateway.ExternalDns c.TlsMode = gateway.TlsMode c.ServiceType = gateway.ServiceType @@ -46,6 +49,7 @@ func PresentGateway(gateway *Gateway, createdBy string) openapi.Gateway { ClusterId: gateway.ClusterId, ReleaseId: gateway.ReleaseId, DatabaseId: gateway.DatabaseId, + ProfileId: openapi.PtrString(gateway.ProfileId), Namespace: gateway.Namespace, ExternalDns: gateway.ExternalDns, TlsMode: gateway.TlsMode, diff --git a/components/api-server/plugins/gateways/profile_test.go b/components/api-server/plugins/gateways/profile_test.go new file mode 100644 index 00000000..a3e3a468 --- /dev/null +++ b/components/api-server/plugins/gateways/profile_test.go @@ -0,0 +1,359 @@ +package gateways + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gorilla/mux" + + "github.com/openshift-online/rh-trex-ai/pkg/api" + rherrors "github.com/openshift-online/rh-trex-ai/pkg/errors" +) + +// noopGatewayEventService satisfies services.EventService for unit tests. +type noopGatewayEventService struct{} + +func (noopGatewayEventService) Get(_ context.Context, _ string) (*api.Event, *rherrors.ServiceError) { + return nil, nil +} +func (noopGatewayEventService) Create(_ context.Context, ev *api.Event) (*api.Event, *rherrors.ServiceError) { + return ev, nil +} +func (noopGatewayEventService) Replace(_ context.Context, ev *api.Event) (*api.Event, *rherrors.ServiceError) { + return ev, nil +} +func (noopGatewayEventService) Delete(_ context.Context, _ string) *rherrors.ServiceError { return nil } +func (noopGatewayEventService) All(_ context.Context) (api.EventList, *rherrors.ServiceError) { + return nil, nil +} +func (noopGatewayEventService) FindByIDs(_ context.Context, _ []string) (api.EventList, *rherrors.ServiceError) { + return nil, nil +} +func (noopGatewayEventService) FindUnreconciled(_ context.Context, _ time.Duration) (api.EventList, *rherrors.ServiceError) { + return nil, nil +} +func (noopGatewayEventService) FindBySourceAndType(_ context.Context, _ string, _ api.EventType) (api.EventList, *rherrors.ServiceError) { + return nil, nil +} + +// settingPlacement is a PlacementResolver that assigns a fixed database_id so +// Create reaches the profile-resolution logic under test. +type settingPlacement struct{ databaseID string } + +func (p settingPlacement) Resolve(_ context.Context, gw *Gateway) error { + gw.DatabaseId = p.databaseID + return nil +} + +// fakeProfileResolver is a configurable ProfileResolver double. +type fakeProfileResolver struct { + clusterDefault string + clusterDefaultErr error + exists bool + existsErr error + + clusterDefaultCalls int +} + +func (f *fakeProfileResolver) ClusterDefaultProfileID(_ context.Context, _ string) (string, error) { + f.clusterDefaultCalls++ + return f.clusterDefault, f.clusterDefaultErr +} + +func (f *fakeProfileResolver) ProfileExists(_ context.Context, _ string) (bool, error) { + return f.exists, f.existsErr +} + +func (f *fakeProfileResolver) ClusterExists(_ context.Context, _ string) (bool, error) { + return true, nil +} + +func newProfileTestService(profiles ProfileResolver) *sqlGatewayService { + return &sqlGatewayService{ + gatewayDao: NewMockGatewayDao(), + events: noopGatewayEventService{}, + placement: settingPlacement{databaseID: "db-assigned"}, + profiles: profiles, + } +} + +// TestGatewayServiceProfileResolution covers the service-level profile +// resolution in Create: a client value wins; otherwise the cluster default is +// used; an empty result is rejected; and a nonexistent profile is rejected. +func TestGatewayServiceProfileResolution(t *testing.T) { + t.Run("client-supplied profile wins over cluster default", func(t *testing.T) { + resolver := &fakeProfileResolver{clusterDefault: "cluster-default", exists: true} + svc := newProfileTestService(resolver) + + gw := &Gateway{Name: "gw", ClusterId: "c-1", ProfileId: "client-profile"} + created, svcErr := svc.Create(context.Background(), gw) + if svcErr != nil { + t.Fatalf("Create() unexpected error: %v", svcErr) + } + if created.ProfileId != "client-profile" { + t.Fatalf("ProfileId = %q, want client-supplied %q", created.ProfileId, "client-profile") + } + if resolver.clusterDefaultCalls != 0 { + t.Fatalf("cluster default consulted %d times, want 0 when client supplies a profile", resolver.clusterDefaultCalls) + } + }) + + t.Run("falls back to cluster default when none supplied", func(t *testing.T) { + resolver := &fakeProfileResolver{clusterDefault: "cluster-default", exists: true} + svc := newProfileTestService(resolver) + + gw := &Gateway{Name: "gw", ClusterId: "c-1"} + created, svcErr := svc.Create(context.Background(), gw) + if svcErr != nil { + t.Fatalf("Create() unexpected error: %v", svcErr) + } + if created.ProfileId != "cluster-default" { + t.Fatalf("ProfileId = %q, want cluster default %q", created.ProfileId, "cluster-default") + } + if resolver.clusterDefaultCalls != 1 { + t.Fatalf("cluster default consulted %d times, want 1", resolver.clusterDefaultCalls) + } + }) + + t.Run("rejects when neither client nor cluster supplies a profile", func(t *testing.T) { + resolver := &fakeProfileResolver{clusterDefault: "", exists: true} + svc := newProfileTestService(resolver) + + _, svcErr := svc.Create(context.Background(), &Gateway{Name: "gw", ClusterId: "c-1"}) + if svcErr == nil { + t.Fatal("Create() = nil error, want HTTP 400 when no profile is resolvable") + } + if svcErr.HttpCode != http.StatusBadRequest { + t.Fatalf("Create() HttpCode = %d, want 400", svcErr.HttpCode) + } + }) + + t.Run("rejects a nonexistent profile on create", func(t *testing.T) { + resolver := &fakeProfileResolver{exists: false} + svc := newProfileTestService(resolver) + + _, svcErr := svc.Create(context.Background(), &Gateway{Name: "gw", ClusterId: "c-1", ProfileId: "ghost"}) + if svcErr == nil { + t.Fatal("Create() = nil error, want HTTP 400 for a nonexistent profile") + } + if svcErr.HttpCode != http.StatusBadRequest { + t.Fatalf("Create() HttpCode = %d, want 400", svcErr.HttpCode) + } + }) +} + +// fakeGatewayService implements GatewayService for handler-level PATCH tests. +type fakeGatewayService struct { + gateway *Gateway + getErr *rherrors.ServiceError + profileExists bool + profileErr *rherrors.ServiceError + clusterExists bool + clusterErr *rherrors.ServiceError + replaceErr *rherrors.ServiceError + replaced *Gateway +} + +func (f *fakeGatewayService) Get(_ context.Context, _ string) (*Gateway, *rherrors.ServiceError) { + return f.gateway, f.getErr +} +func (f *fakeGatewayService) GetUnscoped(_ context.Context, _ string) (*Gateway, *rherrors.ServiceError) { + return f.gateway, f.getErr +} +func (f *fakeGatewayService) Create(_ context.Context, g *Gateway) (*Gateway, *rherrors.ServiceError) { + return g, nil +} +func (f *fakeGatewayService) Replace(_ context.Context, g *Gateway) (*Gateway, *rherrors.ServiceError) { + f.replaced = g + return g, f.replaceErr +} +func (f *fakeGatewayService) Delete(_ context.Context, _ string) *rherrors.ServiceError { return nil } +func (f *fakeGatewayService) All(_ context.Context) (GatewayList, *rherrors.ServiceError) { + return nil, nil +} +func (f *fakeGatewayService) AdjustActiveSandboxCount(_ context.Context, _ string, _ int) (int, *rherrors.ServiceError) { + return 0, nil +} +func (f *fakeGatewayService) SetActiveSandboxCount(_ context.Context, _ string, _ int) (int, *rherrors.ServiceError) { + return 0, nil +} +func (f *fakeGatewayService) FindByIDs(_ context.Context, _ []string) (GatewayList, *rherrors.ServiceError) { + return nil, nil +} +func (f *fakeGatewayService) ProfileExists(_ context.Context, _ string) (bool, *rherrors.ServiceError) { + return f.profileExists, f.profileErr +} +func (f *fakeGatewayService) ClusterExists(_ context.Context, _ string) (bool, *rherrors.ServiceError) { + return f.clusterExists, f.clusterErr +} +func (f *fakeGatewayService) OnUpsert(_ context.Context, _ string) error { return nil } +func (f *fakeGatewayService) OnDelete(_ context.Context, _ string) error { return nil } + +// doProfilePatch drives the PATCH handler through a gorilla mux router so +// mux.Vars are populated, and returns the recorded response. +func doProfilePatch(h *gatewayHandler, gatewayID string, body interface{}) *http.Response { + raw, _ := json.Marshal(body) + req := httptest.NewRequest(http.MethodPatch, "/gateways/"+gatewayID, bytes.NewReader(raw)) + req.Header.Set("Content-Type", "application/json") + + router := mux.NewRouter() + router.HandleFunc("/gateways/{id}", h.Patch).Methods(http.MethodPatch) + + rr := httptest.NewRecorder() + router.ServeHTTP(rr, req) + return rr.Result() +} + +func gatewayWithProfileID(id, profileID string) *Gateway { + gw := &Gateway{Name: "gw-" + id, ProfileId: profileID} + gw.ID = id + return gw +} + +func TestGatewayPatchProfileID_ClearRejected(t *testing.T) { + svc := &fakeGatewayService{gateway: gatewayWithProfileID("gw-1", "current-profile"), profileExists: true, clusterExists: true} + h := &gatewayHandler{gateway: svc} + + resp := doProfilePatch(h, "gw-1", map[string]interface{}{"profile_id": ""}) + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("PATCH profile_id=%q returned %d, want 400", "", resp.StatusCode) + } + if svc.replaced != nil { + t.Fatal("Replace() must not be called when clearing the profile is rejected") + } +} + +func TestGatewayPatchProfileID_NonexistentRejected(t *testing.T) { + svc := &fakeGatewayService{gateway: gatewayWithProfileID("gw-1", "current-profile"), profileExists: false, clusterExists: true} + h := &gatewayHandler{gateway: svc} + + resp := doProfilePatch(h, "gw-1", map[string]interface{}{"profile_id": "nonexistent-id"}) + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("PATCH profile_id=nonexistent returned %d, want 400", resp.StatusCode) + } + if svc.replaced != nil { + t.Fatal("Replace() must not be called when the target profile does not exist") + } +} + +func TestGatewayPatchProfileID_ReassignSucceeds(t *testing.T) { + svc := &fakeGatewayService{gateway: gatewayWithProfileID("gw-1", "old-profile"), profileExists: true, clusterExists: true} + h := &gatewayHandler{gateway: svc} + + resp := doProfilePatch(h, "gw-1", map[string]interface{}{"profile_id": "new-profile"}) + if resp.StatusCode != http.StatusOK { + t.Fatalf("PATCH profile_id=new-profile returned %d, want 200", resp.StatusCode) + } + if svc.replaced == nil { + t.Fatal("Replace() was not called") + } + if svc.replaced.ProfileId != "new-profile" { + t.Fatalf("replaced gateway ProfileId = %q, want %q", svc.replaced.ProfileId, "new-profile") + } +} + +// TestGatewayServiceClusterValidation covers the service-level cluster_id +// validation in Create: a nonexistent cluster_id is rejected with 400, and +// an existing cluster_id passes. +func TestGatewayServiceClusterValidation(t *testing.T) { + t.Run("rejects a nonexistent cluster_id on create", func(t *testing.T) { + resolver := &fakeProfileResolver{exists: true} + svc := newProfileTestService(&clusterRejectingResolver{fakeProfileResolver: resolver}) + + _, svcErr := svc.Create(context.Background(), &Gateway{Name: "gw", ClusterId: "ghost-cluster", ProfileId: "p"}) + if svcErr == nil { + t.Fatal("Create() = nil error, want HTTP 400 for a nonexistent cluster_id") + } + if svcErr.HttpCode != http.StatusBadRequest { + t.Fatalf("Create() HttpCode = %d, want 400", svcErr.HttpCode) + } + }) + + t.Run("accepts a valid cluster_id on create", func(t *testing.T) { + resolver := &fakeProfileResolver{exists: true} + svc := newProfileTestService(resolver) + + gw := &Gateway{Name: "gw", ClusterId: "real-cluster-id", ProfileId: "p"} + created, svcErr := svc.Create(context.Background(), gw) + if svcErr != nil { + t.Fatalf("Create() unexpected error: %v", svcErr) + } + if created.ClusterId != "real-cluster-id" { + t.Fatalf("ClusterId = %q, want %q", created.ClusterId, "real-cluster-id") + } + }) +} + +// clusterRejectingResolver wraps fakeProfileResolver and rejects all cluster lookups. +type clusterRejectingResolver struct { + *fakeProfileResolver +} + +func (r *clusterRejectingResolver) ClusterExists(_ context.Context, _ string) (bool, error) { + return false, nil +} + +// TestGatewayPatchClusterID_NonexistentRejected verifies the PATCH handler rejects +// a cluster_id that does not exist. +func TestGatewayPatchClusterID_NonexistentRejected(t *testing.T) { + svc := &fakeGatewayService{ + gateway: gatewayWithProfileID("gw-1", "current-profile"), + profileExists: true, + clusterExists: false, + } + h := &gatewayHandler{gateway: svc} + + resp := doProfilePatch(h, "gw-1", map[string]interface{}{"cluster_id": "ghost-cluster"}) + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("PATCH cluster_id=ghost returned %d, want 400", resp.StatusCode) + } + if svc.replaced != nil { + t.Fatal("Replace() must not be called when the cluster does not exist") + } +} + +// TestGatewayPatchClusterID_ClearRejected verifies the PATCH handler rejects +// clearing cluster_id with an empty string. +func TestGatewayPatchClusterID_ClearRejected(t *testing.T) { + svc := &fakeGatewayService{ + gateway: gatewayWithProfileID("gw-1", "current-profile"), + profileExists: true, + clusterExists: true, + } + h := &gatewayHandler{gateway: svc} + + resp := doProfilePatch(h, "gw-1", map[string]interface{}{"cluster_id": ""}) + if resp.StatusCode != http.StatusBadRequest { + t.Fatalf("PATCH cluster_id=%q returned %d, want 400", "", resp.StatusCode) + } + if svc.replaced != nil { + t.Fatal("Replace() must not be called when clearing cluster_id is rejected") + } +} + +// TestGatewayPatchClusterID_ReassignSucceeds verifies the PATCH handler accepts +// updating cluster_id to a cluster that exists. +func TestGatewayPatchClusterID_ReassignSucceeds(t *testing.T) { + svc := &fakeGatewayService{ + gateway: gatewayWithProfileID("gw-1", "current-profile"), + profileExists: true, + clusterExists: true, + } + h := &gatewayHandler{gateway: svc} + + resp := doProfilePatch(h, "gw-1", map[string]interface{}{"cluster_id": "new-cluster-id"}) + if resp.StatusCode != http.StatusOK { + t.Fatalf("PATCH cluster_id=new-cluster-id returned %d, want 200", resp.StatusCode) + } + if svc.replaced == nil { + t.Fatal("Replace() was not called") + } + if svc.replaced.ClusterId != "new-cluster-id" { + t.Fatalf("replaced gateway ClusterId = %q, want %q", svc.replaced.ClusterId, "new-cluster-id") + } +} diff --git a/components/api-server/plugins/gateways/service.go b/components/api-server/plugins/gateways/service.go index 6c255ba0..1933acda 100644 --- a/components/api-server/plugins/gateways/service.go +++ b/components/api-server/plugins/gateways/service.go @@ -2,6 +2,7 @@ package gateways import ( "context" + "fmt" "net/http" "github.com/openshift-online/rh-trex-ai/pkg/api" @@ -13,6 +14,20 @@ import ( const gatewaysLockType db.LockType = "gateways" +// ProfileResolver supplies the gateway service with the two GatewayProfile +// facts it needs without depending on the gatewayProfiles package directly: +// the cluster-level default profile used as a create-time fallback, and an +// existence check used to reject assignments the control plane could not fetch. +type ProfileResolver interface { + // ClusterDefaultProfileID returns the profile_id configured as the default + // for the given cluster, or "" if the cluster has no default profile. + ClusterDefaultProfileID(ctx context.Context, clusterID string) (string, error) + // ProfileExists reports whether a GatewayProfile with the given id exists. + ProfileExists(ctx context.Context, profileID string) (bool, error) + // ClusterExists reports whether a ManagedCluster with the given id exists. + ClusterExists(ctx context.Context, clusterID string) (bool, error) +} + type GatewayService interface { Get(ctx context.Context, id string) (*Gateway, *errors.ServiceError) GetUnscoped(ctx context.Context, id string) (*Gateway, *errors.ServiceError) @@ -33,6 +48,16 @@ type GatewayService interface { FindByIDs(ctx context.Context, ids []string) (GatewayList, *errors.ServiceError) + // ProfileExists reports whether the referenced GatewayProfile exists. It is + // used by the PATCH path to reject reassigning a gateway to a profile_id the + // control plane could not fetch. Returns true when no resolver is wired. + ProfileExists(ctx context.Context, profileID string) (bool, *errors.ServiceError) + + // ClusterExists reports whether the referenced ManagedCluster exists. It is + // used by the PATCH path to reject assigning a gateway to a cluster_id that + // does not exist. Returns true when no resolver is wired. + ClusterExists(ctx context.Context, clusterID string) (bool, *errors.ServiceError) + OnUpsert(ctx context.Context, id string) error OnDelete(ctx context.Context, id string) error } @@ -42,12 +67,14 @@ func NewGatewayService( gatewayDao GatewayDao, events services.EventService, placement PlacementResolver, + profiles ProfileResolver, ) GatewayService { return &sqlGatewayService{ lockFactory: lockFactory, gatewayDao: gatewayDao, events: events, placement: placement, + profiles: profiles, } } @@ -58,6 +85,7 @@ type sqlGatewayService struct { gatewayDao GatewayDao events services.EventService placement PlacementResolver + profiles ProfileResolver } func (s *sqlGatewayService) OnUpsert(ctx context.Context, id string) error { @@ -68,7 +96,7 @@ func (s *sqlGatewayService) OnUpsert(ctx context.Context, id string) error { return err } - logger.Infof("Do idempotent somethings with this gateway: %s", gateway.ID) + logger.Infof("Gateway upserted: %s (name=%s namespace=%s)", gateway.ID, gateway.Name, gateway.Namespace) return nil } @@ -96,6 +124,53 @@ func (s *sqlGatewayService) GetUnscoped(ctx context.Context, id string) (*Gatewa } func (s *sqlGatewayService) Create(ctx context.Context, gateway *Gateway) (*Gateway, *errors.ServiceError) { + // A nil resolver disables cluster/profile validation and quota enforcement. + // Production wiring always injects one (see NewServiceLocator), so this path + // is reachable only from tests or a wiring regression; make the latter loud. + if s.profiles == nil { + logger.NewLogger(ctx).Warning("Gateway create proceeding with no ProfileResolver wired; skipping cluster and profile validation (production wiring always injects a resolver)") + } + + // Validate cluster_id and profile_id before calling placement.Resolve, which + // may provision a real ManagedDatabase. Bad requests must be rejected before + // any server-side resources are created. + + // Validate that cluster_id references a real ManagedCluster. + if s.profiles != nil && gateway.ClusterId != "" { + clusterExists, clusterErr := s.profiles.ClusterExists(ctx, gateway.ClusterId) + if clusterErr != nil { + return nil, errors.GeneralError("failed to validate cluster_id: %s", clusterErr) + } + if !clusterExists { + return nil, errors.Validation("cluster %s does not exist", gateway.ClusterId) + } + } + + // Resolve the gateway quota profile. A client-supplied profile_id wins; if + // none was supplied, fall back to the cluster's default profile. A profile + // is required, so if neither source yields one the create is rejected. + if gateway.ProfileId == "" && s.profiles != nil { + clusterDefault, err := s.profiles.ClusterDefaultProfileID(ctx, gateway.ClusterId) + if err != nil { + return nil, errors.GeneralError("failed to resolve cluster default gateway profile: %s", err) + } + gateway.ProfileId = clusterDefault + } + if gateway.ProfileId == "" { + return nil, errors.Validation("profile_id is required: none supplied and cluster %s has no default profile", gateway.ClusterId) + } + // The referenced profile must exist so the control plane can fetch it when + // building the namespace ResourceQuota; otherwise provisioning would block. + if s.profiles != nil { + exists, err := s.profiles.ProfileExists(ctx, gateway.ProfileId) + if err != nil { + return nil, errors.GeneralError("failed to validate gateway profile: %s", err) + } + if !exists { + return nil, errors.Validation("gateway profile %s does not exist", gateway.ProfileId) + } + } + // database_id is server-owned. Clear any value that reached the business // layer from an API client before selecting the configured placement strategy. gateway.DatabaseId = "" @@ -206,6 +281,33 @@ func (s *sqlGatewayService) Delete(ctx context.Context, id string) *errors.Servi return nil } +func (s *sqlGatewayService) ProfileExists(ctx context.Context, profileID string) (bool, *errors.ServiceError) { + if s.profiles == nil { + logger.NewLogger(ctx).Warning(fmt.Sprintf("ProfileExists called with no ProfileResolver wired; skipping profile validation for %s (production wiring always injects a resolver)", profileID)) + return true, nil + } + exists, err := s.profiles.ProfileExists(ctx, profileID) + if err != nil { + return false, errors.GeneralError("failed to validate gateway profile: %s", err) + } + return exists, nil +} + +func (s *sqlGatewayService) ClusterExists(ctx context.Context, clusterID string) (bool, *errors.ServiceError) { + if s.profiles == nil { + logger.NewLogger(ctx).Warning(fmt.Sprintf("ClusterExists called with no ProfileResolver wired; skipping cluster validation for %s (production wiring always injects a resolver)", clusterID)) + return true, nil + } + if clusterID == "" { + return true, nil + } + exists, err := s.profiles.ClusterExists(ctx, clusterID) + if err != nil { + return false, errors.GeneralError("failed to check cluster existence: %s", err) + } + return exists, nil +} + func (s *sqlGatewayService) FindByIDs(ctx context.Context, ids []string) (GatewayList, *errors.ServiceError) { gateways, err := s.gatewayDao.FindByIDs(ctx, ids) if err != nil { diff --git a/components/api-server/plugins/managedClusters/grpc_handler.go b/components/api-server/plugins/managedClusters/grpc_handler.go index e02d2115..1031c45c 100644 --- a/components/api-server/plugins/managedClusters/grpc_handler.go +++ b/components/api-server/plugins/managedClusters/grpc_handler.go @@ -20,10 +20,11 @@ type managedClusterGRPCHandler struct { service ManagedClusterService generic services.GenericService brokerFunc func() *pkgserver.EventBroker + profiles ProfileChecker } -func NewManagedClusterGRPCHandler(svc ManagedClusterService, generic services.GenericService, brokerFunc func() *pkgserver.EventBroker) pb.ManagedClusterServiceServer { - return &managedClusterGRPCHandler{service: svc, generic: generic, brokerFunc: brokerFunc} +func NewManagedClusterGRPCHandler(svc ManagedClusterService, generic services.GenericService, brokerFunc func() *pkgserver.EventBroker, profiles ProfileChecker) pb.ManagedClusterServiceServer { + return &managedClusterGRPCHandler{service: svc, generic: generic, brokerFunc: brokerFunc, profiles: profiles} } func (h *managedClusterGRPCHandler) GetManagedCluster(ctx context.Context, req *pb.GetManagedClusterRequest) (*pb.GetManagedClusterResponse, error) { @@ -49,6 +50,16 @@ func (h *managedClusterGRPCHandler) CreateManagedCluster(ctx context.Context, re return nil, err } + if req.ProfileId != nil && *req.ProfileId != "" { + exists, existsErr := h.profiles.ProfileExists(ctx, *req.ProfileId) + if existsErr != nil { + return nil, grpcutil.ServiceErrorToGRPC(existsErr) + } + if !exists { + return nil, status.Errorf(codes.NotFound, "gateway profile %s does not exist", *req.ProfileId) + } + } + managedCluster := &ManagedCluster{ Name: req.Name, Provider: req.Provider, @@ -56,6 +67,8 @@ func (h *managedClusterGRPCHandler) CreateManagedCluster(ctx context.Context, re KubeconfigSecret: req.KubeconfigSecret, Status: req.Status, ApiServerUrl: req.ApiServerUrl, + ProfileId: req.ProfileId, + DatabaseId: req.DatabaseId, } result, svcErr := h.service.Create(ctx, managedCluster) if svcErr != nil { @@ -121,6 +134,21 @@ func (h *managedClusterGRPCHandler) UpdateManagedCluster(ctx context.Context, re if req.ApiServerUrl != nil { managedCluster.ApiServerUrl = req.ApiServerUrl } + if req.ProfileId != nil { + if *req.ProfileId != "" { + exists, existsErr := h.profiles.ProfileExists(ctx, *req.ProfileId) + if existsErr != nil { + return nil, grpcutil.ServiceErrorToGRPC(existsErr) + } + if !exists { + return nil, status.Errorf(codes.NotFound, "gateway profile %s does not exist", *req.ProfileId) + } + } + managedCluster.ProfileId = req.ProfileId + } + if req.DatabaseId != nil { + managedCluster.DatabaseId = req.DatabaseId + } result, svcErr := h.service.Replace(ctx, managedCluster) if svcErr != nil { return nil, grpcutil.ServiceErrorToGRPC(svcErr) diff --git a/components/api-server/plugins/managedClusters/grpc_presenter.go b/components/api-server/plugins/managedClusters/grpc_presenter.go index 672727d1..22e442c0 100644 --- a/components/api-server/plugins/managedClusters/grpc_presenter.go +++ b/components/api-server/plugins/managedClusters/grpc_presenter.go @@ -20,5 +20,7 @@ func managedClusterToProto(d *ManagedCluster) *pb.ManagedCluster { KubeconfigSecret: d.KubeconfigSecret, Status: d.Status, ApiServerUrl: d.ApiServerUrl, + ProfileId: d.ProfileId, + DatabaseId: d.DatabaseId, } } diff --git a/components/api-server/plugins/managedClusters/handler.go b/components/api-server/plugins/managedClusters/handler.go index 682f9f3e..e973f0ac 100644 --- a/components/api-server/plugins/managedClusters/handler.go +++ b/components/api-server/plugins/managedClusters/handler.go @@ -1,6 +1,7 @@ package managedClusters import ( + "context" "net/http" "github.com/gorilla/mux" @@ -12,17 +13,26 @@ import ( "github.com/openshift-online/rh-trex-ai/pkg/services" ) +// ProfileChecker validates that a referenced GatewayProfile exists so bad +// profile_id values are rejected at the API boundary rather than discovered +// later as a confusing reconciliation failure. +type ProfileChecker interface { + ProfileExists(ctx context.Context, profileID string) (bool, *errors.ServiceError) +} + var _ handlers.RestHandler = managedClusterHandler{} type managedClusterHandler struct { managedCluster ManagedClusterService generic services.GenericService + profiles ProfileChecker } -func NewManagedClusterHandler(managedCluster ManagedClusterService, generic services.GenericService) *managedClusterHandler { +func NewManagedClusterHandler(managedCluster ManagedClusterService, generic services.GenericService, profiles ProfileChecker) *managedClusterHandler { return &managedClusterHandler{ managedCluster: managedCluster, generic: generic, + profiles: profiles, } } @@ -36,6 +46,15 @@ func (h managedClusterHandler) Create(w http.ResponseWriter, r *http.Request) { Action: func() (interface{}, *errors.ServiceError) { ctx := r.Context() managedClusterModel := ConvertManagedCluster(managedCluster) + if managedClusterModel.ProfileId != nil && *managedClusterModel.ProfileId != "" { + exists, existsErr := h.profiles.ProfileExists(ctx, *managedClusterModel.ProfileId) + if existsErr != nil { + return nil, existsErr + } + if !exists { + return nil, errors.Validation("gateway profile %s does not exist", *managedClusterModel.ProfileId) + } + } managedClusterModel, err := h.managedCluster.Create(ctx, managedClusterModel) if err != nil { return nil, err @@ -80,6 +99,21 @@ func (h managedClusterHandler) Patch(w http.ResponseWriter, r *http.Request) { if patch.ApiServerUrl != nil { found.ApiServerUrl = patch.ApiServerUrl } + if patch.ProfileId != nil { + if *patch.ProfileId != "" { + exists, existsErr := h.profiles.ProfileExists(ctx, *patch.ProfileId) + if existsErr != nil { + return nil, existsErr + } + if !exists { + return nil, errors.Validation("gateway profile %s does not exist", *patch.ProfileId) + } + } + found.ProfileId = patch.ProfileId + } + if patch.DatabaseId != nil { + found.DatabaseId = patch.DatabaseId + } managedClusterModel, err := h.managedCluster.Replace(ctx, found) if err != nil { diff --git a/components/api-server/plugins/managedClusters/migration.go b/components/api-server/plugins/managedClusters/migration.go index fec58550..740144a7 100644 --- a/components/api-server/plugins/managedClusters/migration.go +++ b/components/api-server/plugins/managedClusters/migration.go @@ -30,6 +30,29 @@ func migration() *gormigrate.Migration { } } +func migrationAddProfileAndDatabaseID() *gormigrate.Migration { + type ManagedCluster struct { + db.Model + ProfileId *string + DatabaseId *string + } + + return &gormigrate.Migration{ + ID: "2026082800000003", + Migrate: func(tx *gorm.DB) error { + return tx.AutoMigrate(&ManagedCluster{}) + }, + Rollback: func(tx *gorm.DB) error { + for _, col := range []string{"profile_id", "database_id"} { + if err := tx.Migrator().DropColumn(&ManagedCluster{}, col); err != nil { + return err + } + } + return nil + }, + } +} + func migrationDropFleetId() *gormigrate.Migration { type ManagedCluster struct{ db.Model } diff --git a/components/api-server/plugins/managedClusters/model.go b/components/api-server/plugins/managedClusters/model.go index ca4e54cf..9b0059fb 100644 --- a/components/api-server/plugins/managedClusters/model.go +++ b/components/api-server/plugins/managedClusters/model.go @@ -13,6 +13,8 @@ type ManagedCluster struct { KubeconfigSecret string `json:"kubeconfig_secret"` Status *string `json:"status"` ApiServerUrl *string `json:"api_server_url"` + ProfileId *string `json:"profile_id"` + DatabaseId *string `json:"database_id"` } type ManagedClusterList []*ManagedCluster @@ -38,4 +40,6 @@ type ManagedClusterPatchRequest struct { KubeconfigSecret *string `json:"kubeconfig_secret,omitempty"` Status *string `json:"status,omitempty"` ApiServerUrl *string `json:"api_server_url,omitempty"` + ProfileId *string `json:"profile_id,omitempty"` + DatabaseId *string `json:"database_id,omitempty"` } diff --git a/components/api-server/plugins/managedClusters/plugin.go b/components/api-server/plugins/managedClusters/plugin.go index 6ef37b7e..179c2848 100644 --- a/components/api-server/plugins/managedClusters/plugin.go +++ b/components/api-server/plugins/managedClusters/plugin.go @@ -1,24 +1,45 @@ package managedClusters import ( + "context" "net/http" "github.com/gorilla/mux" "google.golang.org/grpc" pb "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1" + "github.com/openshift-online/hypershell/components/api-server/plugins/gatewayProfiles" "github.com/openshift-online/rh-trex-ai/pkg/api" "github.com/openshift-online/rh-trex-ai/pkg/api/presenters" "github.com/openshift-online/rh-trex-ai/pkg/auth" "github.com/openshift-online/rh-trex-ai/pkg/controllers" "github.com/openshift-online/rh-trex-ai/pkg/db" "github.com/openshift-online/rh-trex-ai/pkg/environments" + "github.com/openshift-online/rh-trex-ai/pkg/errors" "github.com/openshift-online/rh-trex-ai/pkg/registry" pkgserver "github.com/openshift-online/rh-trex-ai/pkg/server" "github.com/openshift-online/rh-trex-ai/plugins/events" "github.com/openshift-online/rh-trex-ai/plugins/generic" ) +type profileCheckerAdapter struct { + profiles gatewayProfiles.GatewayProfileService +} + +func (a *profileCheckerAdapter) ProfileExists(ctx context.Context, profileID string) (bool, *errors.ServiceError) { + if a.profiles == nil || profileID == "" { + return false, nil + } + _, svcErr := a.profiles.Get(ctx, profileID) + if svcErr != nil { + if svcErr.Is404() { + return false, nil + } + return false, svcErr + } + return true, nil +} + type ServiceLocator func() ManagedClusterService func NewServiceLocator(env *environments.Env) ServiceLocator { @@ -49,7 +70,8 @@ func init() { pkgserver.RegisterRoutes("managedClusters", func(apiV1Router *mux.Router, services pkgserver.ServicesInterface, authMiddleware environments.JWTMiddleware, authzMiddleware auth.AuthorizationMiddleware) { envServices := services.(*environments.Services) - managedClusterHandler := NewManagedClusterHandler(Service(envServices), generic.Service(envServices)) + profileChecker := &profileCheckerAdapter{profiles: gatewayProfiles.Service(envServices)} + managedClusterHandler := NewManagedClusterHandler(Service(envServices), generic.Service(envServices), profileChecker) managedClustersRouter := apiV1Router.PathPrefix("/managed_clusters").Subrouter() managedClustersRouter.HandleFunc("", managedClusterHandler.List).Methods(http.MethodGet) @@ -84,7 +106,8 @@ func init() { } return nil } - pb.RegisterManagedClusterServiceServer(grpcServer, NewManagedClusterGRPCHandler(managedClusterService, genericService, brokerFunc)) + profileChecker := &profileCheckerAdapter{profiles: gatewayProfiles.Service(envServices)} + pb.RegisterManagedClusterServiceServer(grpcServer, NewManagedClusterGRPCHandler(managedClusterService, genericService, brokerFunc, profileChecker)) }) presenters.RegisterPath(ManagedCluster{}, "managed_clusters") @@ -93,5 +116,6 @@ func init() { presenters.RegisterKind(&ManagedCluster{}, "ManagedCluster") db.RegisterMigration(migration()) + db.RegisterMigration(migrationAddProfileAndDatabaseID()) db.RegisterMigration(migrationDropFleetId()) } diff --git a/components/api-server/plugins/managedClusters/presenter.go b/components/api-server/plugins/managedClusters/presenter.go index f736c8b9..506942bb 100644 --- a/components/api-server/plugins/managedClusters/presenter.go +++ b/components/api-server/plugins/managedClusters/presenter.go @@ -19,6 +19,8 @@ func ConvertManagedCluster(managedCluster openapi.ManagedCluster) *ManagedCluste c.KubeconfigSecret = managedCluster.KubeconfigSecret c.Status = managedCluster.Status c.ApiServerUrl = managedCluster.ApiServerUrl + c.ProfileId = managedCluster.ProfileId + c.DatabaseId = managedCluster.DatabaseId if managedCluster.CreatedAt != nil { c.CreatedAt = *managedCluster.CreatedAt @@ -42,5 +44,7 @@ func PresentManagedCluster(managedCluster *ManagedCluster) openapi.ManagedCluste KubeconfigSecret: managedCluster.KubeconfigSecret, Status: managedCluster.Status, ApiServerUrl: managedCluster.ApiServerUrl, + ProfileId: managedCluster.ProfileId, + DatabaseId: managedCluster.DatabaseId, } } diff --git a/components/api-server/proto/hypershell/v1/gateway_profiles.proto b/components/api-server/proto/hypershell/v1/gateway_profiles.proto new file mode 100644 index 00000000..9422ad48 --- /dev/null +++ b/components/api-server/proto/hypershell/v1/gateway_profiles.proto @@ -0,0 +1,102 @@ +syntax = "proto3"; + +package hypershell.v1; + +option go_package = "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1;hypershell_v1"; + +import "hypershell/v1/common.proto"; + +message GatewayProfile { + ObjectReference metadata = 1; + string name = 2; + optional string description = 3; + optional string cpu_request_total = 4; + optional string cpu_limit_total = 5; + optional string memory_request_total = 6; + optional string memory_limit_total = 7; + optional string ephemeral_storage_total = 8; + optional int32 pod_count = 9; + optional int32 pvc_count = 10; + optional string container_cpu_request_default = 11; + optional string container_cpu_limit_max = 12; + optional string container_memory_request_default = 13; + optional string container_memory_limit_max = 14; +} + +message CreateGatewayProfileRequest { + string name = 1; + optional string description = 2; + optional string cpu_request_total = 3; + optional string cpu_limit_total = 4; + optional string memory_request_total = 5; + optional string memory_limit_total = 6; + optional string ephemeral_storage_total = 7; + optional int32 pod_count = 8; + optional int32 pvc_count = 9; + optional string container_cpu_request_default = 10; + optional string container_cpu_limit_max = 11; + optional string container_memory_request_default = 12; + optional string container_memory_limit_max = 13; +} + +message CreateGatewayProfileResponse { + GatewayProfile gateway_profile = 1; +} + +message GetGatewayProfileRequest { + string id = 1; +} + +message GetGatewayProfileResponse { + GatewayProfile gateway_profile = 1; +} + +message UpdateGatewayProfileRequest { + string id = 1; + optional string name = 2; + optional string description = 3; + optional string cpu_request_total = 4; + optional string cpu_limit_total = 5; + optional string memory_request_total = 6; + optional string memory_limit_total = 7; + optional string ephemeral_storage_total = 8; + optional int32 pod_count = 9; + optional int32 pvc_count = 10; + optional string container_cpu_request_default = 11; + optional string container_cpu_limit_max = 12; + optional string container_memory_request_default = 13; + optional string container_memory_limit_max = 14; +} + +message UpdateGatewayProfileResponse { + GatewayProfile gateway_profile = 1; +} + +message DeleteGatewayProfileRequest { + string id = 1; +} + +message ListGatewayProfilesRequest { + int32 page = 1; + int32 size = 2; +} + +message ListGatewayProfilesResponse { + repeated GatewayProfile items = 1; + ListMeta metadata = 2; +} + +message DeleteGatewayProfileResponse {} + +// GatewayProfileService intentionally exposes no Watch RPC: the control plane +// does not reconcile GatewayProfile changes. A gateway's quota changes only when +// its profile_id is reassigned (a gateway update, delivered through the existing +// gateway watch stream). The control plane fetches profiles on demand via the +// unary GetGatewayProfile RPC while reconciling a gateway. +service GatewayProfileService { + rpc GetGatewayProfile(GetGatewayProfileRequest) returns (GetGatewayProfileResponse); + rpc CreateGatewayProfile(CreateGatewayProfileRequest) returns (CreateGatewayProfileResponse); + rpc UpdateGatewayProfile(UpdateGatewayProfileRequest) returns (UpdateGatewayProfileResponse); + rpc DeleteGatewayProfile(DeleteGatewayProfileRequest) returns (DeleteGatewayProfileResponse); + rpc ListGatewayProfiles(ListGatewayProfilesRequest) returns (ListGatewayProfilesResponse); +} diff --git a/components/api-server/proto/hypershell/v1/gateways.proto b/components/api-server/proto/hypershell/v1/gateways.proto index b515414f..8ee4f096 100644 --- a/components/api-server/proto/hypershell/v1/gateways.proto +++ b/components/api-server/proto/hypershell/v1/gateways.proto @@ -29,6 +29,7 @@ message Gateway { optional string credential_driver = 20; optional int32 active_sandbox_count = 21; optional string console_address = 22; + optional string profile_id = 23; } message CreateGatewayRequest { @@ -49,6 +50,7 @@ message CreateGatewayRequest { optional string oidc = 14; optional string route = 15; optional string credential_driver = 17; + optional string profile_id = 18; } message CreateGatewayResponse { @@ -88,6 +90,7 @@ message UpdateGatewayRequest { // so the whole-row UpdateGateway path can never clobber it. See // openshell-gateway-sandbox-count.spec.md. optional string console_address = 20; + optional string profile_id = 21; } message UpdateGatewayResponse { diff --git a/components/api-server/proto/hypershell/v1/managed_clusters.proto b/components/api-server/proto/hypershell/v1/managed_clusters.proto index 9529f29c..4527d9e2 100644 --- a/components/api-server/proto/hypershell/v1/managed_clusters.proto +++ b/components/api-server/proto/hypershell/v1/managed_clusters.proto @@ -16,6 +16,8 @@ message ManagedCluster { string kubeconfig_secret = 6; optional string status = 7; optional string api_server_url = 8; + optional string profile_id = 9; + optional string database_id = 10; } message CreateManagedClusterRequest { @@ -27,6 +29,8 @@ message CreateManagedClusterRequest { string kubeconfig_secret = 5; optional string status = 6; optional string api_server_url = 7; + optional string profile_id = 8; + optional string database_id = 9; } message CreateManagedClusterResponse { @@ -51,6 +55,8 @@ message UpdateManagedClusterRequest { optional string kubeconfig_secret = 6; optional string status = 7; optional string api_server_url = 8; + optional string profile_id = 9; + optional string database_id = 10; } message UpdateManagedClusterResponse { diff --git a/components/cli/cmd/hypershell/create/cmd.go b/components/cli/cmd/hypershell/create/cmd.go index 05b5c937..00fab7e3 100644 --- a/components/cli/cmd/hypershell/create/cmd.go +++ b/components/cli/cmd/hypershell/create/cmd.go @@ -5,6 +5,7 @@ import ( "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/create/gateway" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/create/gatewayNetwork" + "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/create/gatewayProfile" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/create/gatewayRelease" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/create/managedCluster" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/create/managedDatabase" @@ -22,6 +23,7 @@ var Cmd = &cobra.Command{ func init() { Cmd.AddCommand(gateway.Cmd) Cmd.AddCommand(gatewayNetwork.Cmd) + Cmd.AddCommand(gatewayProfile.Cmd) Cmd.AddCommand(gatewayRelease.Cmd) Cmd.AddCommand(managedCluster.Cmd) Cmd.AddCommand(managedDatabase.Cmd) diff --git a/components/cli/cmd/hypershell/create/gateway/cmd.go b/components/cli/cmd/hypershell/create/gateway/cmd.go index 6a42f213..9bed3c5f 100644 --- a/components/cli/cmd/hypershell/create/gateway/cmd.go +++ b/components/cli/cmd/hypershell/create/gateway/cmd.go @@ -16,20 +16,22 @@ import ( ) var args struct { - clusterId string - databaseId string - externalDns string - image string - name string - phase string - releaseId string - route string - serverDnsNames string - serviceType string - status string - supervisorImage string - tlsMode string - bodyFile string + clusterId string + credentialDriver string + databaseId string + externalDns string + image string + name string + phase string + profileId string + releaseId string + route string + serverDnsNames string + serviceType string + status string + supervisorImage string + tlsMode string + bodyFile string } var Cmd = &cobra.Command{ @@ -37,7 +39,7 @@ var Cmd = &cobra.Command{ Short: "Create a gateway", Long: "Create a new gateway.\n\n" + "Examples:\n" + - " hsctl create gateway --cluster-id --database-id --external-dns --image --name --phase --release-id --route --server-dns-names --service-type --status --supervisor-image --tls-mode \n" + + " hsctl create gateway --cluster-id --credential-driver --database-id --external-dns --image --name --phase --profile-id --release-id --route --server-dns-names --service-type --status --supervisor-image --tls-mode \n" + " hsctl create gateway --body request.json", Args: cobra.NoArgs, RunE: run, @@ -46,11 +48,13 @@ var Cmd = &cobra.Command{ func init() { fs := Cmd.Flags() fs.StringVar(&args.clusterId, "cluster-id", "", "cluster_id value.") + fs.StringVar(&args.credentialDriver, "credential-driver", "", "credential_driver value.") fs.StringVar(&args.databaseId, "database-id", "", "database_id value.") fs.StringVar(&args.externalDns, "external-dns", "", "external_dns value.") fs.StringVar(&args.image, "image", "", "image value.") fs.StringVar(&args.name, "name", "", "name value.") fs.StringVar(&args.phase, "phase", "", "phase value.") + fs.StringVar(&args.profileId, "profile-id", "", "profile_id value.") fs.StringVar(&args.releaseId, "release-id", "", "release_id value.") fs.StringVar(&args.route, "route", "", "route value.") fs.StringVar(&args.serverDnsNames, "server-dns-names", "", "server_dns_names value.") @@ -85,6 +89,9 @@ func run(cmd *cobra.Command, argv []string) error { if args.clusterId != "" { request["cluster_id"] = args.clusterId } + if args.credentialDriver != "" { + request["credential_driver"] = args.credentialDriver + } if args.databaseId != "" { request["database_id"] = args.databaseId } @@ -100,6 +107,9 @@ func run(cmd *cobra.Command, argv []string) error { if args.phase != "" { request["phase"] = args.phase } + if args.profileId != "" { + request["profile_id"] = args.profileId + } if args.releaseId != "" { request["release_id"] = args.releaseId } diff --git a/components/cli/cmd/hypershell/create/gatewayProfile/cmd.go b/components/cli/cmd/hypershell/create/gatewayProfile/cmd.go new file mode 100644 index 00000000..a24b4654 --- /dev/null +++ b/components/cli/cmd/hypershell/create/gatewayProfile/cmd.go @@ -0,0 +1,146 @@ +package gatewayProfile + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + + "github.com/openshift-online/hypershell/components/cli/pkg/config" + "github.com/openshift-online/hypershell/components/cli/pkg/connection" + "github.com/openshift-online/hypershell/components/cli/pkg/dump" + "github.com/openshift-online/hypershell/components/cli/pkg/urls" +) + +var args struct { + containerCpuLimitMax string + containerCpuRequestDefault string + containerMemoryLimitMax string + containerMemoryRequestDefault string + cpuLimitTotal string + cpuRequestTotal string + description string + ephemeralStorageTotal string + memoryLimitTotal string + memoryRequestTotal string + name string + podCount int + pvcCount int + bodyFile string +} + +var Cmd = &cobra.Command{ + Use: "gatewayProfile [flags]", + Short: "Create a gatewayProfile", + Long: "Create a new gatewayProfile.\n\n" + + "Examples:\n" + + " hypershell create gatewayProfile --container-cpu-limit-max --container-cpu-request-default --container-memory-limit-max --container-memory-request-default --cpu-limit-total --cpu-request-total --description --ephemeral-storage-total --memory-limit-total --memory-request-total --name --pod-count --pvc-count \n" + + " hypershell create gatewayProfile --body request.json", + Args: cobra.NoArgs, + RunE: run, +} + +func init() { + fs := Cmd.Flags() + fs.StringVar(&args.containerCpuLimitMax, "container-cpu-limit-max", "", "container_cpu_limit_max value.") + fs.StringVar(&args.containerCpuRequestDefault, "container-cpu-request-default", "", "container_cpu_request_default value.") + fs.StringVar(&args.containerMemoryLimitMax, "container-memory-limit-max", "", "container_memory_limit_max value.") + fs.StringVar(&args.containerMemoryRequestDefault, "container-memory-request-default", "", "container_memory_request_default value.") + fs.StringVar(&args.cpuLimitTotal, "cpu-limit-total", "", "cpu_limit_total value.") + fs.StringVar(&args.cpuRequestTotal, "cpu-request-total", "", "cpu_request_total value.") + fs.StringVar(&args.description, "description", "", "description value.") + fs.StringVar(&args.ephemeralStorageTotal, "ephemeral-storage-total", "", "ephemeral_storage_total value.") + fs.StringVar(&args.memoryLimitTotal, "memory-limit-total", "", "memory_limit_total value.") + fs.StringVar(&args.memoryRequestTotal, "memory-request-total", "", "memory_request_total value.") + fs.StringVar(&args.name, "name", "", "name value.") + fs.IntVar(&args.podCount, "pod-count", 0, "pod_count value.") + fs.IntVar(&args.pvcCount, "pvc-count", 0, "pvc_count value.") + fs.StringVar(&args.bodyFile, "body", "", "File containing the request body as JSON.") +} + +func run(cmd *cobra.Command, argv []string) error { + cfg, err := config.Load() + if err != nil { + return err + } + + conn, err := connection.NewConnection().Config(cfg).Build() + if err != nil { + return err + } + defer conn.Close() + + var body []byte + + if args.bodyFile != "" { + body, err = os.ReadFile(args.bodyFile) + if err != nil { + return fmt.Errorf("can't read body file: %v", err) + } + } else { + request := map[string]interface{}{} + if args.containerCpuLimitMax != "" { + request["container_cpu_limit_max"] = args.containerCpuLimitMax + } + if args.containerCpuRequestDefault != "" { + request["container_cpu_request_default"] = args.containerCpuRequestDefault + } + if args.containerMemoryLimitMax != "" { + request["container_memory_limit_max"] = args.containerMemoryLimitMax + } + if args.containerMemoryRequestDefault != "" { + request["container_memory_request_default"] = args.containerMemoryRequestDefault + } + if args.cpuLimitTotal != "" { + request["cpu_limit_total"] = args.cpuLimitTotal + } + if args.cpuRequestTotal != "" { + request["cpu_request_total"] = args.cpuRequestTotal + } + if args.description != "" { + request["description"] = args.description + } + if args.ephemeralStorageTotal != "" { + request["ephemeral_storage_total"] = args.ephemeralStorageTotal + } + if args.memoryLimitTotal != "" { + request["memory_limit_total"] = args.memoryLimitTotal + } + if args.memoryRequestTotal != "" { + request["memory_request_total"] = args.memoryRequestTotal + } + if args.name != "" { + request["name"] = args.name + } + if args.podCount != 0 { + request["pod_count"] = args.podCount + } + if args.pvcCount != 0 { + request["pvc_count"] = args.pvcCount + } + body, err = json.Marshal(request) + if err != nil { + return fmt.Errorf("can't marshal request: %v", err) + } + } + + resp, err := conn.Post(urls.GatewayProfilesPath, bytes.NewReader(body)) + if err != nil { + return fmt.Errorf("can't create gatewayProfile: %v", err) + } + defer resp.Body.Close() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("can't read response: %v", err) + } + + if resp.StatusCode != 201 && resp.StatusCode != 200 { + return fmt.Errorf("API returned %d: %s", resp.StatusCode, string(respBody)) + } + + return dump.Pretty(os.Stdout, respBody) +} diff --git a/components/cli/cmd/hypershell/create/managedCluster/cmd.go b/components/cli/cmd/hypershell/create/managedCluster/cmd.go index e431a2b5..538feef2 100644 --- a/components/cli/cmd/hypershell/create/managedCluster/cmd.go +++ b/components/cli/cmd/hypershell/create/managedCluster/cmd.go @@ -17,8 +17,10 @@ import ( var args struct { apiServerUrl string + databaseId string kubeconfigSecret string name string + profileId string provider string region string status string @@ -30,7 +32,7 @@ var Cmd = &cobra.Command{ Short: "Create a managedCluster", Long: "Create a new managedCluster.\n\n" + "Examples:\n" + - " hsctl create managedCluster --api-server-url --kubeconfig-secret --name --provider --region --status \n" + + " hsctl create managedCluster --api-server-url --database-id --kubeconfig-secret --name --profile-id --provider --region --status \n" + " hsctl create managedCluster --body request.json", Args: cobra.NoArgs, RunE: run, @@ -39,8 +41,10 @@ var Cmd = &cobra.Command{ func init() { fs := Cmd.Flags() fs.StringVar(&args.apiServerUrl, "api-server-url", "", "api_server_url value.") + fs.StringVar(&args.databaseId, "database-id", "", "database_id value.") fs.StringVar(&args.kubeconfigSecret, "kubeconfig-secret", "", "kubeconfig_secret value.") fs.StringVar(&args.name, "name", "", "name value.") + fs.StringVar(&args.profileId, "profile-id", "", "profile_id value.") fs.StringVar(&args.provider, "provider", "", "provider value.") fs.StringVar(&args.region, "region", "", "region value.") fs.StringVar(&args.status, "status", "", "status value.") @@ -71,12 +75,18 @@ func run(cmd *cobra.Command, argv []string) error { if args.apiServerUrl != "" { request["api_server_url"] = args.apiServerUrl } + if args.databaseId != "" { + request["database_id"] = args.databaseId + } if args.kubeconfigSecret != "" { request["kubeconfig_secret"] = args.kubeconfigSecret } if args.name != "" { request["name"] = args.name } + if args.profileId != "" { + request["profile_id"] = args.profileId + } if args.provider != "" { request["provider"] = args.provider } diff --git a/components/cli/cmd/hypershell/delete/cmd.go b/components/cli/cmd/hypershell/delete/cmd.go index 5d448521..2000b12a 100644 --- a/components/cli/cmd/hypershell/delete/cmd.go +++ b/components/cli/cmd/hypershell/delete/cmd.go @@ -5,6 +5,7 @@ import ( "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/delete/gateway" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/delete/gatewayNetwork" + "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/delete/gatewayProfile" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/delete/gatewayRelease" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/delete/managedCluster" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/delete/managedDatabase" @@ -21,6 +22,7 @@ var Cmd = &cobra.Command{ func init() { Cmd.AddCommand(gateway.Cmd) Cmd.AddCommand(gatewayNetwork.Cmd) + Cmd.AddCommand(gatewayProfile.Cmd) Cmd.AddCommand(gatewayRelease.Cmd) Cmd.AddCommand(managedCluster.Cmd) Cmd.AddCommand(managedDatabase.Cmd) diff --git a/components/cli/cmd/hypershell/delete/gatewayProfile/cmd.go b/components/cli/cmd/hypershell/delete/gatewayProfile/cmd.go new file mode 100644 index 00000000..71b8370b --- /dev/null +++ b/components/cli/cmd/hypershell/delete/gatewayProfile/cmd.go @@ -0,0 +1,71 @@ +package gatewayProfile + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/openshift-online/hypershell/components/cli/pkg/config" + "github.com/openshift-online/hypershell/components/cli/pkg/connection" + "github.com/openshift-online/hypershell/components/cli/pkg/urls" +) + +var args struct { + yes bool +} + +var Cmd = &cobra.Command{ + Use: "gatewayProfile ID [flags]", + Short: "Delete a gatewayProfile", + Long: "Delete a gatewayProfile by ID.\n\n" + + "Examples:\n" + + " hypershell delete gatewayProfile 2abc123\n" + + " hypershell delete gatewayProfile 2abc123 --yes", + Args: cobra.ExactArgs(1), + RunE: run, +} + +func init() { + fs := Cmd.Flags() + fs.BoolVar(&args.yes, "yes", false, "Skip confirmation prompt.") +} + +func run(cmd *cobra.Command, argv []string) error { + gatewayProfileID := argv[0] + + if !args.yes { + fmt.Fprintf(os.Stderr, "Delete gatewayProfile %s? [y/N]: ", gatewayProfileID) + var response string + fmt.Scanln(&response) + if response != "y" && response != "Y" && response != "yes" { + fmt.Fprintf(os.Stderr, "Cancelled.\n") + return nil + } + } + + cfg, err := config.Load() + if err != nil { + return err + } + + conn, err := connection.NewConnection().Config(cfg).Build() + if err != nil { + return err + } + defer conn.Close() + + path := urls.GatewayProfilesPath + "/" + gatewayProfileID + resp, err := conn.Delete(path) + if err != nil { + return fmt.Errorf("can't delete gatewayProfile: %v", err) + } + defer resp.Body.Close() + + if resp.StatusCode != 204 && resp.StatusCode != 200 { + return fmt.Errorf("API returned %d", resp.StatusCode) + } + + fmt.Fprintf(os.Stdout, "GatewayProfile %s deleted.\n", gatewayProfileID) + return nil +} diff --git a/components/cli/cmd/hypershell/get/cmd.go b/components/cli/cmd/hypershell/get/cmd.go index ffe65aef..74824bf3 100644 --- a/components/cli/cmd/hypershell/get/cmd.go +++ b/components/cli/cmd/hypershell/get/cmd.go @@ -5,6 +5,7 @@ import ( "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/get/gateway" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/get/gatewayNetwork" + "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/get/gatewayProfile" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/get/gatewayRelease" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/get/managedCluster" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/get/managedDatabase" @@ -22,6 +23,7 @@ var Cmd = &cobra.Command{ func init() { Cmd.AddCommand(gateway.Cmd) Cmd.AddCommand(gatewayNetwork.Cmd) + Cmd.AddCommand(gatewayProfile.Cmd) Cmd.AddCommand(gatewayRelease.Cmd) Cmd.AddCommand(managedCluster.Cmd) Cmd.AddCommand(managedDatabase.Cmd) diff --git a/components/cli/cmd/hypershell/get/gatewayProfile/cmd.go b/components/cli/cmd/hypershell/get/gatewayProfile/cmd.go new file mode 100644 index 00000000..4c36e762 --- /dev/null +++ b/components/cli/cmd/hypershell/get/gatewayProfile/cmd.go @@ -0,0 +1,55 @@ +package gatewayProfile + +import ( + "fmt" + "io" + "os" + + "github.com/spf13/cobra" + + "github.com/openshift-online/hypershell/components/cli/pkg/config" + "github.com/openshift-online/hypershell/components/cli/pkg/connection" + "github.com/openshift-online/hypershell/components/cli/pkg/dump" + "github.com/openshift-online/hypershell/components/cli/pkg/urls" +) + +var Cmd = &cobra.Command{ + Use: "gatewayProfile ID", + Aliases: []string{"gatewayProfiles"}, + Short: "Get a gatewayProfile by ID", + Long: "Get a gatewayProfile by ID and display its details", + Args: cobra.ExactArgs(1), + RunE: run, +} + +func run(cmd *cobra.Command, argv []string) error { + id := argv[0] + + cfg, err := config.Load() + if err != nil { + return err + } + + conn, err := connection.NewConnection().Config(cfg).Build() + if err != nil { + return err + } + defer conn.Close() + + resp, err := conn.Get(urls.GatewayProfilePath(id), nil) + if err != nil { + return fmt.Errorf("can't retrieve gatewayProfile: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("can't read response: %v", err) + } + + if resp.StatusCode != 200 { + return fmt.Errorf("API returned %d: %s", resp.StatusCode, string(body)) + } + + return dump.Pretty(os.Stdout, body) +} diff --git a/components/cli/cmd/hypershell/list/cmd.go b/components/cli/cmd/hypershell/list/cmd.go index db8f1df5..365ed88a 100644 --- a/components/cli/cmd/hypershell/list/cmd.go +++ b/components/cli/cmd/hypershell/list/cmd.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/list/gatewayNetworks" + "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/list/gatewayProfiles" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/list/gatewayReleases" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/list/gateways" "github.com/openshift-online/hypershell/components/cli/cmd/hypershell/list/managedClusters" @@ -22,6 +23,7 @@ var Cmd = &cobra.Command{ func init() { Cmd.AddCommand(gateways.Cmd) Cmd.AddCommand(gatewayNetworks.Cmd) + Cmd.AddCommand(gatewayProfiles.Cmd) Cmd.AddCommand(gatewayReleases.Cmd) Cmd.AddCommand(managedClusters.Cmd) Cmd.AddCommand(managedDatabases.Cmd) diff --git a/components/cli/cmd/hypershell/list/gatewayProfiles/cmd.go b/components/cli/cmd/hypershell/list/gatewayProfiles/cmd.go new file mode 100644 index 00000000..ed4ebe7e --- /dev/null +++ b/components/cli/cmd/hypershell/list/gatewayProfiles/cmd.go @@ -0,0 +1,149 @@ +package gatewayProfiles + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/spf13/cobra" + + "github.com/openshift-online/hypershell/components/cli/pkg/arguments" + "github.com/openshift-online/hypershell/components/cli/pkg/config" + "github.com/openshift-online/hypershell/components/cli/pkg/connection" + "github.com/openshift-online/hypershell/components/cli/pkg/dump" + "github.com/openshift-online/hypershell/components/cli/pkg/output" + "github.com/openshift-online/hypershell/components/cli/pkg/urls" +) + +var args struct { + parameter []string + noHeaders bool + columns string + outputFmt string + search string + orderBy string +} + +var Cmd = &cobra.Command{ + Use: "gatewayProfiles [flags]", + Aliases: []string{"gatewayProfile"}, + Short: "List gatewayProfiles", + Long: "List gatewayProfiles, optionally filtering by search query", + Args: cobra.NoArgs, + RunE: run, +} + +func init() { + fs := Cmd.Flags() + arguments.AddParameterFlag(fs, &args.parameter) + arguments.AddNoHeadersFlag(fs, &args.noHeaders) + arguments.AddColumnsFlag(fs, &args.columns, "id, container_cpu_limit_max, container_cpu_request_default, container_memory_limit_max, container_memory_request_default, created_at") + arguments.AddOutputFlag(fs, &args.outputFmt) + fs.StringVar(&args.search, "search", "", "Search filter expression.") + fs.StringVar(&args.orderBy, "order-by", "", "Order by expression.") +} + +func run(cmd *cobra.Command, argv []string) error { + ctx := context.Background() + + cfg, err := config.Load() + if err != nil { + return err + } + + conn, err := connection.NewConnection().Config(cfg).Build() + if err != nil { + return err + } + defer conn.Close() + + searchQuery := args.search + for _, param := range args.parameter { + name, value := arguments.ParseNameValuePair(param) + if name == "search" { + if searchQuery != "" { + searchQuery += " and " + } + searchQuery += value + } + } + + if args.outputFmt == "json" { + return listJSON(conn, searchQuery) + } + + printer, err := output.NewPrinter(). + Writer(os.Stdout). + Pager(cfg.Pager). + Build(ctx) + if err != nil { + return err + } + defer printer.Close() + + table, err := printer.NewTable(). + Name("gatewayProfiles"). + Columns(args.columns). + Build(ctx) + if err != nil { + return err + } + defer table.Close() + + if !args.noHeaders { + table.WriteHeaders() + } + + size := 100 + page := 1 + for { + resp, err := conn.List(urls.GatewayProfilesPath, page, size, searchQuery, args.orderBy) + if err != nil { + return fmt.Errorf("can't retrieve gatewayProfiles: %v", err) + } + + for _, item := range resp.Items { + err = table.WriteRawObject(item) + if err != nil { + return err + } + } + + if resp.Size < size { + break + } + page++ + } + + return nil +} + +func listJSON(conn *connection.Connection, search string) error { + size := 100 + page := 1 + var allItems []json.RawMessage + + for { + resp, err := conn.List(urls.GatewayProfilesPath, page, size, search, args.orderBy) + if err != nil { + return fmt.Errorf("can't retrieve gatewayProfiles: %v", err) + } + allItems = append(allItems, resp.Items...) + if resp.Size < size { + break + } + page++ + } + + result := map[string]interface{}{ + "kind": "GatewayProfileList", + "total": len(allItems), + "items": allItems, + } + body, err := json.Marshal(result) + if err != nil { + return err + } + return dump.Pretty(os.Stdout, body) +} diff --git a/components/cli/cmd/hypershell/list/managedClusters/cmd.go b/components/cli/cmd/hypershell/list/managedClusters/cmd.go index dfe6355a..3a197b8e 100644 --- a/components/cli/cmd/hypershell/list/managedClusters/cmd.go +++ b/components/cli/cmd/hypershell/list/managedClusters/cmd.go @@ -38,7 +38,7 @@ func init() { fs := Cmd.Flags() arguments.AddParameterFlag(fs, &args.parameter) arguments.AddNoHeadersFlag(fs, &args.noHeaders) - arguments.AddColumnsFlag(fs, &args.columns, "id, api_server_url, kubeconfig_secret, name, created_at") + arguments.AddColumnsFlag(fs, &args.columns, "id, api_server_url, database_id, kubeconfig_secret, name, created_at") arguments.AddOutputFlag(fs, &args.outputFmt) fs.StringVar(&args.search, "search", "", "Search filter expression.") fs.StringVar(&args.orderBy, "order-by", "", "Order by expression.") diff --git a/components/cli/pkg/urls/urls.go b/components/cli/pkg/urls/urls.go index 95a0ff9e..da1f7ae6 100644 --- a/components/cli/pkg/urls/urls.go +++ b/components/cli/pkg/urls/urls.go @@ -4,6 +4,7 @@ const ( APIPrefix = "/api/hypershell/v1" GatewaysPath = APIPrefix + "/gateways" GatewayNetworksPath = APIPrefix + "/gateway_networks" + GatewayProfilesPath = APIPrefix + "/gateway_profiles" GatewayReleasesPath = APIPrefix + "/gateway_releases" ManagedClustersPath = APIPrefix + "/managed_clusters" ManagedDatabasesPath = APIPrefix + "/managed_databases" @@ -19,6 +20,10 @@ func GatewayNetworkPath(id string) string { return GatewayNetworksPath + "/" + id } +func GatewayProfilePath(id string) string { + return GatewayProfilesPath + "/" + id +} + func GatewayReleasePath(id string) string { return GatewayReleasesPath + "/" + id } diff --git a/components/control-plane/internal/gateway/config.go b/components/control-plane/internal/gateway/config.go index 09b66055..1bcc23ec 100644 --- a/components/control-plane/internal/gateway/config.go +++ b/components/control-plane/internal/gateway/config.go @@ -217,6 +217,11 @@ type ReconcileOpts struct { // resources, so an in-flight pass does not recreate them behind a concurrent // health-loop teardown. Nil disables the re-check (the pass proceeds). RouteStillDesired func(ctx context.Context) (bool, error) + // Quota is the resource quota derived from the gateway's GatewayProfile. + // Non-nil drives creation/update of the namespace ResourceQuota and + // LimitRange; nil (legacy gateway with no profile) reconciles toward + // absence of both managed objects. + Quota *QuotaConfig } // KeycloakClientAPI is the subset of keycloak.Client needed by the gateway package. diff --git a/components/control-plane/internal/gateway/console.go b/components/control-plane/internal/gateway/console.go index cc9689cd..5077ad15 100644 --- a/components/control-plane/internal/gateway/console.go +++ b/components/control-plane/internal/gateway/console.go @@ -506,7 +506,7 @@ func consoleSecurityContext() map[string]interface{} { func consoleResources() map[string]interface{} { return map[string]interface{}{ "requests": map[string]interface{}{"cpu": "10m", "memory": "64Mi"}, - "limits": map[string]interface{}{"memory": "128Mi"}, + "limits": map[string]interface{}{"cpu": "500m", "memory": "128Mi"}, } } diff --git a/components/control-plane/internal/gateway/quota.go b/components/control-plane/internal/gateway/quota.go new file mode 100644 index 00000000..e34ac05e --- /dev/null +++ b/components/control-plane/internal/gateway/quota.go @@ -0,0 +1,279 @@ +package gateway + +import ( + "context" + "fmt" + "log" + + corev1 "k8s.io/api/core/v1" + apiequality "k8s.io/apimachinery/pkg/api/equality" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes" +) + +// Names of the quota objects the control plane manages in each gateway +// namespace. Kept stable so update-or-create and delete-when-empty always +// address the same objects across reconciliations. +const ( + GatewayResourceQuotaName = "hypershell-gateway-quota" + GatewayLimitRangeName = "hypershell-gateway-limits" +) + +// QuotaConfig is the control-plane-side translation of a GatewayProfile. It +// carries the namespace-level ResourceQuota totals and the container-level +// LimitRange defaults/maxima the reconciler enforces on a gateway namespace. +// String fields are Kubernetes resource quantities ("2", "500m", "8Gi"); an +// empty string means "not set". Count fields are non-negative; zero means +// "not set". A nil *QuotaConfig means the gateway has no profile (legacy), +// which reconciles toward absence of both objects. +type QuotaConfig struct { + CPURequestTotal string + CPULimitTotal string + MemoryRequestTotal string + MemoryLimitTotal string + EphemeralStorageTotal string + PodCount int32 + PVCCount int32 + + ContainerCPURequestDefault string + ContainerCPULimitMax string + ContainerMemoryRequestDefault string + ContainerMemoryLimitMax string +} + +// ReconcileNamespaceQuota drives the namespace's managed ResourceQuota and +// LimitRange toward the desired state derived from quota. It is idempotent: +// create when absent and desired is non-empty, update when the spec diverges, +// no-op when it matches, and delete the managed object when the desired state +// is empty (nil quota, or a profile that omits the relevant fields). Only +// objects carrying the managed label are ever deleted. +func ReconcileNamespaceQuota(ctx context.Context, clientset kubernetes.Interface, namespace string, quota *QuotaConfig) error { + if err := reconcileResourceQuota(ctx, clientset, namespace, quota); err != nil { + return err + } + return reconcileLimitRange(ctx, clientset, namespace, quota) +} + +func reconcileResourceQuota(ctx context.Context, clientset kubernetes.Interface, namespace string, quota *QuotaConfig) error { + desired, nonEmpty, err := desiredResourceQuotaSpec(quota) + if err != nil { + return fmt.Errorf("build ResourceQuota spec for %s: %w", namespace, err) + } + + client := clientset.CoreV1().ResourceQuotas(namespace) + existing, getErr := client.Get(ctx, GatewayResourceQuotaName, metav1.GetOptions{}) + if getErr != nil { + if !k8serrors.IsNotFound(getErr) { + return fmt.Errorf("get ResourceQuota %s in %s: %w", GatewayResourceQuotaName, namespace, getErr) + } + if !nonEmpty { + return nil + } + rq := &corev1.ResourceQuota{ + ObjectMeta: metav1.ObjectMeta{ + Name: GatewayResourceQuotaName, + Namespace: namespace, + Labels: managedResourceLabels(), + }, + Spec: corev1.ResourceQuotaSpec{Hard: desired}, + } + if _, err := client.Create(ctx, rq, metav1.CreateOptions{}); err != nil { + return fmt.Errorf("create ResourceQuota %s in %s: %w", GatewayResourceQuotaName, namespace, err) + } + log.Printf("INFO created ResourceQuota %s in namespace %s", GatewayResourceQuotaName, namespace) + return nil + } + + if !nonEmpty { + if !isManagedObject(existing.Labels) { + return nil + } + if err := client.Delete(ctx, GatewayResourceQuotaName, metav1.DeleteOptions{}); err != nil && !k8serrors.IsNotFound(err) { + return fmt.Errorf("delete ResourceQuota %s in %s: %w", GatewayResourceQuotaName, namespace, err) + } + log.Printf("INFO deleted ResourceQuota %s in namespace %s", GatewayResourceQuotaName, namespace) + return nil + } + + if apiequality.Semantic.DeepEqual(existing.Spec.Hard, desired) { + return nil + } + existing.Spec.Hard = desired + if existing.Labels == nil { + existing.Labels = managedResourceLabels() + } + if _, err := client.Update(ctx, existing, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("update ResourceQuota %s in %s: %w", GatewayResourceQuotaName, namespace, err) + } + log.Printf("INFO updated ResourceQuota %s in namespace %s", GatewayResourceQuotaName, namespace) + return nil +} + +func reconcileLimitRange(ctx context.Context, clientset kubernetes.Interface, namespace string, quota *QuotaConfig) error { + desired, nonEmpty, err := desiredLimitRangeItem(quota) + if err != nil { + return fmt.Errorf("build LimitRange spec for %s: %w", namespace, err) + } + + client := clientset.CoreV1().LimitRanges(namespace) + existing, getErr := client.Get(ctx, GatewayLimitRangeName, metav1.GetOptions{}) + if getErr != nil { + if !k8serrors.IsNotFound(getErr) { + return fmt.Errorf("get LimitRange %s in %s: %w", GatewayLimitRangeName, namespace, getErr) + } + if !nonEmpty { + return nil + } + lr := &corev1.LimitRange{ + ObjectMeta: metav1.ObjectMeta{ + Name: GatewayLimitRangeName, + Namespace: namespace, + Labels: managedResourceLabels(), + }, + Spec: corev1.LimitRangeSpec{Limits: []corev1.LimitRangeItem{desired}}, + } + if _, err := client.Create(ctx, lr, metav1.CreateOptions{}); err != nil { + return fmt.Errorf("create LimitRange %s in %s: %w", GatewayLimitRangeName, namespace, err) + } + log.Printf("INFO created LimitRange %s in namespace %s", GatewayLimitRangeName, namespace) + return nil + } + + if !nonEmpty { + if !isManagedObject(existing.Labels) { + return nil + } + if err := client.Delete(ctx, GatewayLimitRangeName, metav1.DeleteOptions{}); err != nil && !k8serrors.IsNotFound(err) { + return fmt.Errorf("delete LimitRange %s in %s: %w", GatewayLimitRangeName, namespace, err) + } + log.Printf("INFO deleted LimitRange %s in namespace %s", GatewayLimitRangeName, namespace) + return nil + } + + desiredLimits := []corev1.LimitRangeItem{desired} + if apiequality.Semantic.DeepEqual(existing.Spec.Limits, desiredLimits) { + return nil + } + existing.Spec.Limits = desiredLimits + if existing.Labels == nil { + existing.Labels = managedResourceLabels() + } + if _, err := client.Update(ctx, existing, metav1.UpdateOptions{}); err != nil { + return fmt.Errorf("update LimitRange %s in %s: %w", GatewayLimitRangeName, namespace, err) + } + log.Printf("INFO updated LimitRange %s in namespace %s", GatewayLimitRangeName, namespace) + return nil +} + +// desiredResourceQuotaSpec builds the ResourceQuota hard map from quota, +// omitting empty/zero fields. It reports whether any field is set; an empty +// map means no ResourceQuota should exist. +func desiredResourceQuotaSpec(quota *QuotaConfig) (corev1.ResourceList, bool, error) { + hard := corev1.ResourceList{} + if quota == nil { + return hard, false, nil + } + + quantities := []struct { + name corev1.ResourceName + value string + }{ + {corev1.ResourceRequestsCPU, quota.CPURequestTotal}, + {corev1.ResourceLimitsCPU, quota.CPULimitTotal}, + {corev1.ResourceRequestsMemory, quota.MemoryRequestTotal}, + {corev1.ResourceLimitsMemory, quota.MemoryLimitTotal}, + {corev1.ResourceRequestsEphemeralStorage, quota.EphemeralStorageTotal}, + } + for _, q := range quantities { + if q.value == "" { + continue + } + parsed, err := resource.ParseQuantity(q.value) + if err != nil { + return nil, false, fmt.Errorf("invalid quantity %q for %s: %w", q.value, q.name, err) + } + hard[q.name] = parsed + } + if quota.PodCount > 0 { + hard[corev1.ResourcePods] = *resource.NewQuantity(int64(quota.PodCount), resource.DecimalSI) + } + if quota.PVCCount > 0 { + hard[corev1.ResourcePersistentVolumeClaims] = *resource.NewQuantity(int64(quota.PVCCount), resource.DecimalSI) + } + + return hard, len(hard) > 0, nil +} + +// desiredLimitRangeItem builds the Container LimitRangeItem from quota, +// omitting empty fields. It reports whether any container-level field is set; +// when false no LimitRange should exist. +func desiredLimitRangeItem(quota *QuotaConfig) (corev1.LimitRangeItem, bool, error) { + item := corev1.LimitRangeItem{ + Type: corev1.LimitTypeContainer, + DefaultRequest: corev1.ResourceList{}, + Max: corev1.ResourceList{}, + } + if quota == nil { + return item, false, nil + } + + defaults := []struct { + name corev1.ResourceName + value string + }{ + {corev1.ResourceCPU, quota.ContainerCPURequestDefault}, + {corev1.ResourceMemory, quota.ContainerMemoryRequestDefault}, + } + for _, d := range defaults { + if d.value == "" { + continue + } + parsed, err := resource.ParseQuantity(d.value) + if err != nil { + return item, false, fmt.Errorf("invalid defaultRequest quantity %q for %s: %w", d.value, d.name, err) + } + item.DefaultRequest[d.name] = parsed + } + + maxima := []struct { + name corev1.ResourceName + value string + }{ + {corev1.ResourceCPU, quota.ContainerCPULimitMax}, + {corev1.ResourceMemory, quota.ContainerMemoryLimitMax}, + } + for _, m := range maxima { + if m.value == "" { + continue + } + parsed, err := resource.ParseQuantity(m.value) + if err != nil { + return item, false, fmt.Errorf("invalid max quantity %q for %s: %w", m.value, m.name, err) + } + item.Max[m.name] = parsed + } + + nonEmpty := len(item.DefaultRequest) > 0 || len(item.Max) > 0 + // Drop empty sub-maps so a match against a freshly-decoded object (which + // leaves nil maps) compares equal and does not trigger spurious updates. + if len(item.DefaultRequest) == 0 { + item.DefaultRequest = nil + } + if len(item.Max) == 0 { + item.Max = nil + } + return item, nonEmpty, nil +} + +func managedResourceLabels() map[string]string { + return map[string]string{ + ManagedByLabel: ManagedByValue, + ManagedLabel: ManagedLabelValue, + } +} + +func isManagedObject(labels map[string]string) bool { + return labels[ManagedLabel] == ManagedLabelValue +} diff --git a/components/control-plane/internal/gateway/quota_reconcile_test.go b/components/control-plane/internal/gateway/quota_reconcile_test.go new file mode 100644 index 00000000..95e16b61 --- /dev/null +++ b/components/control-plane/internal/gateway/quota_reconcile_test.go @@ -0,0 +1,186 @@ +package gateway + +import ( + "context" + "testing" + + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +const testNS = "openshell-test" + +func getQuota(t *testing.T, cs *fake.Clientset) (*corev1.ResourceQuota, bool) { + t.Helper() + q, err := cs.CoreV1().ResourceQuotas(testNS).Get(context.Background(), GatewayResourceQuotaName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + return nil, false + } + if err != nil { + t.Fatalf("get quota: %v", err) + } + return q, true +} + +func getLimitRange(t *testing.T, cs *fake.Clientset) (*corev1.LimitRange, bool) { + t.Helper() + lr, err := cs.CoreV1().LimitRanges(testNS).Get(context.Background(), GatewayLimitRangeName, metav1.GetOptions{}) + if k8serrors.IsNotFound(err) { + return nil, false + } + if err != nil { + t.Fatalf("get limitrange: %v", err) + } + return lr, true +} + +// A nil config reconciles toward absence: both managed objects are deleted. +func TestReconcileNamespaceQuota_NilConfigDeletesBoth(t *testing.T) { + cs := fake.NewSimpleClientset() + ctx := context.Background() + + cfg := &QuotaConfig{ + CPURequestTotal: "2", + MemoryLimitTotal: "8Gi", + ContainerCPURequestDefault: "100m", + ContainerMemoryLimitMax: "4Gi", + } + if err := ReconcileNamespaceQuota(ctx, cs, testNS, cfg); err != nil { + t.Fatalf("reconcile create: %v", err) + } + if _, ok := getQuota(t, cs); !ok { + t.Fatal("expected ResourceQuota to be created") + } + if _, ok := getLimitRange(t, cs); !ok { + t.Fatal("expected LimitRange to be created") + } + + if err := ReconcileNamespaceQuota(ctx, cs, testNS, nil); err != nil { + t.Fatalf("reconcile to absent: %v", err) + } + if _, ok := getQuota(t, cs); ok { + t.Fatal("expected ResourceQuota to be deleted when profile cleared") + } + if _, ok := getLimitRange(t, cs); ok { + t.Fatal("expected LimitRange to be deleted when profile cleared") + } +} + +// Reassigning to a profile that only sets namespace-level totals drops the +// LimitRange but keeps the ResourceQuota. +func TestReconcileNamespaceQuota_ReassignDropsLimitRangeOnly(t *testing.T) { + cs := fake.NewSimpleClientset() + ctx := context.Background() + + full := &QuotaConfig{ + CPURequestTotal: "2", + ContainerCPURequestDefault: "100m", + } + if err := ReconcileNamespaceQuota(ctx, cs, testNS, full); err != nil { + t.Fatalf("reconcile create: %v", err) + } + if _, ok := getLimitRange(t, cs); !ok { + t.Fatal("expected LimitRange to be created") + } + + quotaOnly := &QuotaConfig{CPURequestTotal: "2"} + if err := ReconcileNamespaceQuota(ctx, cs, testNS, quotaOnly); err != nil { + t.Fatalf("reconcile reassign: %v", err) + } + if _, ok := getQuota(t, cs); !ok { + t.Fatal("expected ResourceQuota to remain") + } + if _, ok := getLimitRange(t, cs); ok { + t.Fatal("expected LimitRange to be removed when container fields drop") + } +} + +// Create writes the expected quantities, and an update-on-diverge rewrites them. +func TestReconcileNamespaceQuota_CreateWritesQuantitiesAndUpdatesOnDiverge(t *testing.T) { + cs := fake.NewSimpleClientset() + ctx := context.Background() + + small := &QuotaConfig{CPURequestTotal: "2"} + if err := ReconcileNamespaceQuota(ctx, cs, testNS, small); err != nil { + t.Fatalf("reconcile create: %v", err) + } + q, ok := getQuota(t, cs) + if !ok { + t.Fatal("expected ResourceQuota to be created") + } + initialQty := q.Spec.Hard[corev1.ResourceRequestsCPU] + if initialQty.String() != "2" { + t.Fatalf("initial requests.cpu = %q, want %q", initialQty.String(), "2") + } + if !isManagedObject(q.Labels) { + t.Fatalf("expected created ResourceQuota to carry the managed label, got %v", q.Labels) + } + + large := &QuotaConfig{CPURequestTotal: "4"} + if err := ReconcileNamespaceQuota(ctx, cs, testNS, large); err != nil { + t.Fatalf("reconcile update: %v", err) + } + q, ok = getQuota(t, cs) + if !ok { + t.Fatal("expected ResourceQuota to still exist after update") + } + updatedQty := q.Spec.Hard[corev1.ResourceRequestsCPU] + if updatedQty.String() != "4" { + t.Fatalf("after reassign: requests.cpu = %q, want %q", updatedQty.String(), "4") + } +} + +// A second reconcile with an unchanged config must perform no writes; this +// guards the apiequality.Semantic.DeepEqual no-op behavior. +func TestReconcileNamespaceQuota_IdempotentWhenUnchanged(t *testing.T) { + cs := fake.NewSimpleClientset() + ctx := context.Background() + + cfg := &QuotaConfig{CPURequestTotal: "2", ContainerCPURequestDefault: "100m"} + if err := ReconcileNamespaceQuota(ctx, cs, testNS, cfg); err != nil { + t.Fatalf("first reconcile: %v", err) + } + actionsAfterFirst := len(cs.Actions()) + + if err := ReconcileNamespaceQuota(ctx, cs, testNS, cfg); err != nil { + t.Fatalf("second reconcile: %v", err) + } + + for _, a := range cs.Actions()[actionsAfterFirst:] { + if a.GetVerb() == "create" || a.GetVerb() == "update" { + t.Fatalf("second reconcile performed %q on %s, want no writes when spec is unchanged", + a.GetVerb(), a.GetResource().Resource) + } + } +} + +// An operator-owned object carrying the reserved name but WITHOUT the managed +// label must not be deleted or modified when the desired state is empty. +func TestReconcileNamespaceQuota_LeavesUnmanagedObjectsAlone(t *testing.T) { + unmanagedRQ := &corev1.ResourceQuota{ + ObjectMeta: metav1.ObjectMeta{Name: GatewayResourceQuotaName, Namespace: testNS}, + Spec: corev1.ResourceQuotaSpec{Hard: corev1.ResourceList{corev1.ResourcePods: resource.MustParse("5")}}, + } + unmanagedLR := &corev1.LimitRange{ + ObjectMeta: metav1.ObjectMeta{Name: GatewayLimitRangeName, Namespace: testNS}, + Spec: corev1.LimitRangeSpec{Limits: []corev1.LimitRangeItem{{ + Type: corev1.LimitTypeContainer, + Max: corev1.ResourceList{corev1.ResourceCPU: resource.MustParse("1")}, + }}}, + } + cs := fake.NewSimpleClientset(unmanagedRQ, unmanagedLR) + ctx := context.Background() + + if err := ReconcileNamespaceQuota(ctx, cs, testNS, nil); err != nil { + t.Fatalf("reconcile to absent: %v", err) + } + if _, ok := getQuota(t, cs); !ok { + t.Fatal("expected unmanaged ResourceQuota to be left intact") + } + if _, ok := getLimitRange(t, cs); !ok { + t.Fatal("expected unmanaged LimitRange to be left intact") + } +} diff --git a/components/control-plane/internal/gateway/quota_test.go b/components/control-plane/internal/gateway/quota_test.go new file mode 100644 index 00000000..8d9334ca --- /dev/null +++ b/components/control-plane/internal/gateway/quota_test.go @@ -0,0 +1,196 @@ +package gateway + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" +) + +func TestDesiredResourceQuotaSpec(t *testing.T) { + tests := []struct { + name string + quota *QuotaConfig + wantHard map[corev1.ResourceName]string + wantSet bool + }{ + { + name: "nil quota is empty", + quota: nil, + wantSet: false, + }, + { + name: "all fields unset is empty", + quota: &QuotaConfig{}, + wantSet: false, + }, + { + // Spec § Configuration Examples, Example 1. + name: "dev-small totals and pods", + quota: &QuotaConfig{ + CPURequestTotal: "1", + CPULimitTotal: "2", + MemoryRequestTotal: "2Gi", + MemoryLimitTotal: "4Gi", + PodCount: 10, + }, + wantHard: map[corev1.ResourceName]string{ + corev1.ResourceRequestsCPU: "1", + corev1.ResourceLimitsCPU: "2", + corev1.ResourceRequestsMemory: "2Gi", + corev1.ResourceLimitsMemory: "4Gi", + corev1.ResourcePods: "10", + }, + wantSet: true, + }, + { + // Spec § Configuration Examples, Example 3: pod count only. + name: "pod count only", + quota: &QuotaConfig{PodCount: 20}, + wantHard: map[corev1.ResourceName]string{ + corev1.ResourcePods: "20", + }, + wantSet: true, + }, + { + name: "ephemeral storage and pvc", + quota: &QuotaConfig{ + EphemeralStorageTotal: "50Gi", + PVCCount: 10, + }, + wantHard: map[corev1.ResourceName]string{ + corev1.ResourceRequestsEphemeralStorage: "50Gi", + corev1.ResourcePersistentVolumeClaims: "10", + }, + wantSet: true, + }, + { + name: "zero counts are not set", + quota: &QuotaConfig{PodCount: 0, PVCCount: 0}, + wantSet: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + hard, set, err := desiredResourceQuotaSpec(tt.quota) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if set != tt.wantSet { + t.Fatalf("nonEmpty = %v, want %v", set, tt.wantSet) + } + if len(hard) != len(tt.wantHard) { + t.Fatalf("hard has %d entries %v, want %d %v", len(hard), hard, len(tt.wantHard), tt.wantHard) + } + for name, want := range tt.wantHard { + got, ok := hard[name] + if !ok { + t.Errorf("missing %s", name) + continue + } + wantQ := resource.MustParse(want) + if got.Cmp(wantQ) != 0 { + t.Errorf("%s = %s, want %s", name, got.String(), want) + } + } + }) + } +} + +func TestDesiredResourceQuotaSpecInvalidQuantity(t *testing.T) { + _, _, err := desiredResourceQuotaSpec(&QuotaConfig{CPURequestTotal: "tow"}) + if err == nil { + t.Fatal("expected error for invalid quantity, got nil") + } +} + +func TestDesiredLimitRangeItem(t *testing.T) { + tests := []struct { + name string + quota *QuotaConfig + wantDefault map[corev1.ResourceName]string + wantMax map[corev1.ResourceName]string + wantSet bool + }{ + { + name: "nil quota is empty", + quota: nil, + wantSet: false, + }, + { + name: "no container fields is empty", + quota: &QuotaConfig{CPURequestTotal: "1", PodCount: 5}, + wantSet: false, + }, + { + // Spec § Configuration Examples, Example 2. + name: "defaults and maxima", + quota: &QuotaConfig{ + ContainerCPURequestDefault: "500m", + ContainerCPULimitMax: "4", + ContainerMemoryRequestDefault: "512Mi", + ContainerMemoryLimitMax: "8Gi", + }, + wantDefault: map[corev1.ResourceName]string{ + corev1.ResourceCPU: "500m", + corev1.ResourceMemory: "512Mi", + }, + wantMax: map[corev1.ResourceName]string{ + corev1.ResourceCPU: "4", + corev1.ResourceMemory: "8Gi", + }, + wantSet: true, + }, + { + // Spec § Configuration Examples, Example 1: defaults only, no max. + name: "defaults only", + quota: &QuotaConfig{ + ContainerCPURequestDefault: "50m", + ContainerMemoryRequestDefault: "64Mi", + }, + wantDefault: map[corev1.ResourceName]string{ + corev1.ResourceCPU: "50m", + corev1.ResourceMemory: "64Mi", + }, + wantSet: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + item, set, err := desiredLimitRangeItem(tt.quota) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if set != tt.wantSet { + t.Fatalf("nonEmpty = %v, want %v", set, tt.wantSet) + } + if !tt.wantSet { + return + } + if item.Type != corev1.LimitTypeContainer { + t.Errorf("type = %s, want Container", item.Type) + } + assertResourceList(t, "defaultRequest", item.DefaultRequest, tt.wantDefault) + assertResourceList(t, "max", item.Max, tt.wantMax) + }) + } +} + +func assertResourceList(t *testing.T, label string, got corev1.ResourceList, want map[corev1.ResourceName]string) { + t.Helper() + if len(got) != len(want) { + t.Fatalf("%s has %d entries %v, want %d %v", label, len(got), got, len(want), want) + } + for name, w := range want { + g, ok := got[name] + if !ok { + t.Errorf("%s missing %s", label, name) + continue + } + if g.Cmp(resource.MustParse(w)) != 0 { + t.Errorf("%s[%s] = %s, want %s", label, name, g.String(), w) + } + } +} diff --git a/components/control-plane/internal/gateway/reconciler.go b/components/control-plane/internal/gateway/reconciler.go index 353f4e30..e4381e73 100644 --- a/components/control-plane/internal/gateway/reconciler.go +++ b/components/control-plane/internal/gateway/reconciler.go @@ -65,6 +65,13 @@ func ReconcileGateway( } } + // Apply the ResourceQuota / LimitRange before deploying any workload so the + // gateway's own pods are admitted under the same quota and receive the + // LimitRange container defaults. Reconciles toward absence when Quota is nil. + if err := ReconcileNamespaceQuota(ctx, clientset, nsConfig.Name, opts.Quota); err != nil { + return fmt.Errorf("reconcile namespace quota in %s: %w", nsConfig.Name, err) + } + if err := ValidateGatewayConfig(nsConfig.Gateway); err != nil { return fmt.Errorf("invalid gateway configuration: %w", err) } diff --git a/components/control-plane/internal/reconciler/quota_fetch_test.go b/components/control-plane/internal/reconciler/quota_fetch_test.go new file mode 100644 index 00000000..53adccc9 --- /dev/null +++ b/components/control-plane/internal/reconciler/quota_fetch_test.go @@ -0,0 +1,158 @@ +package reconciler + +import ( + "context" + "errors" + "testing" + + pb "github.com/openshift-online/hypershell/components/api-server/pkg/api/grpc/hypershell/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// fakeGatewayProfileClient is a hand-written test double for +// pb.GatewayProfileServiceClient. Only GetGatewayProfile is exercised by +// resolveGatewayProfileFromClient; the rest satisfy the interface. +type fakeGatewayProfileClient struct { + resp *pb.GetGatewayProfileResponse + err error + + calledWithID string +} + +func (f *fakeGatewayProfileClient) GetGatewayProfile(ctx context.Context, in *pb.GetGatewayProfileRequest, opts ...grpc.CallOption) (*pb.GetGatewayProfileResponse, error) { + f.calledWithID = in.GetId() + return f.resp, f.err +} + +func (f *fakeGatewayProfileClient) CreateGatewayProfile(ctx context.Context, in *pb.CreateGatewayProfileRequest, opts ...grpc.CallOption) (*pb.CreateGatewayProfileResponse, error) { + return nil, errors.New("not implemented") +} + +func (f *fakeGatewayProfileClient) UpdateGatewayProfile(ctx context.Context, in *pb.UpdateGatewayProfileRequest, opts ...grpc.CallOption) (*pb.UpdateGatewayProfileResponse, error) { + return nil, errors.New("not implemented") +} + +func (f *fakeGatewayProfileClient) DeleteGatewayProfile(ctx context.Context, in *pb.DeleteGatewayProfileRequest, opts ...grpc.CallOption) (*pb.DeleteGatewayProfileResponse, error) { + return nil, errors.New("not implemented") +} + +func (f *fakeGatewayProfileClient) ListGatewayProfiles(ctx context.Context, in *pb.ListGatewayProfilesRequest, opts ...grpc.CallOption) (*pb.ListGatewayProfilesResponse, error) { + return nil, errors.New("not implemented") +} + +func strptr(s string) *string { return &s } +func i32ptr(i int32) *int32 { return &i } + +func TestResolveGatewayProfileFromClient(t *testing.T) { + t.Run("empty profile_id returns nil config and no error", func(t *testing.T) { + client := &fakeGatewayProfileClient{err: errors.New("must not be called")} + cfg, err := resolveGatewayProfileFromClient(context.Background(), "", client) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if cfg != nil { + t.Fatalf("expected nil config, got %+v", cfg) + } + if client.calledWithID != "" { + t.Fatalf("expected client not to be called, got id %q", client.calledWithID) + } + }) + + t.Run("NotFound is terminal and blocks provisioning", func(t *testing.T) { + client := &fakeGatewayProfileClient{err: status.Error(codes.NotFound, "profile not found")} + cfg, err := resolveGatewayProfileFromClient(context.Background(), "p-1", client) + if err == nil { + t.Fatal("expected an error when the profile is not found") + } + if cfg != nil { + t.Fatalf("expected nil config on error, got %+v", cfg) + } + if !errors.Is(err, errTerminalProfile) { + t.Fatalf("expected NotFound to be terminal, got %v", err) + } + }) + + t.Run("transient error blocks provisioning but is not terminal", func(t *testing.T) { + client := &fakeGatewayProfileClient{err: status.Error(codes.Unavailable, "api server down")} + cfg, err := resolveGatewayProfileFromClient(context.Background(), "p-1", client) + if err == nil { + t.Fatal("expected an error when the fetch fails transiently") + } + if cfg != nil { + t.Fatalf("expected nil config on error, got %+v", cfg) + } + if errors.Is(err, errTerminalProfile) { + t.Fatalf("expected Unavailable to be transient, got terminal: %v", err) + } + }) + + t.Run("non-status error is treated as transient", func(t *testing.T) { + client := &fakeGatewayProfileClient{err: errors.New("connection reset")} + _, err := resolveGatewayProfileFromClient(context.Background(), "p-1", client) + if err == nil { + t.Fatal("expected an error when the fetch fails") + } + if errors.Is(err, errTerminalProfile) { + t.Fatalf("expected a non-status error to be transient, got terminal: %v", err) + } + }) + + t.Run("nil profile in response is a terminal error", func(t *testing.T) { + client := &fakeGatewayProfileClient{resp: &pb.GetGatewayProfileResponse{}} + _, err := resolveGatewayProfileFromClient(context.Background(), "p-1", client) + if err == nil { + t.Fatal("expected an error when the response profile is nil") + } + if !errors.Is(err, errTerminalProfile) { + t.Fatalf("expected empty payload to be terminal, got %v", err) + } + }) + + t.Run("success maps all fields", func(t *testing.T) { + client := &fakeGatewayProfileClient{resp: &pb.GetGatewayProfileResponse{ + GatewayProfile: &pb.GatewayProfile{ + CpuRequestTotal: strptr("2"), + CpuLimitTotal: strptr("4"), + MemoryRequestTotal: strptr("4Gi"), + MemoryLimitTotal: strptr("8Gi"), + EphemeralStorageTotal: strptr("10Gi"), + PodCount: i32ptr(10), + PvcCount: i32ptr(5), + ContainerCpuRequestDefault: strptr("100m"), + ContainerCpuLimitMax: strptr("2"), + ContainerMemoryRequestDefault: strptr("128Mi"), + ContainerMemoryLimitMax: strptr("4Gi"), + }, + }} + cfg, err := resolveGatewayProfileFromClient(context.Background(), "p-1", client) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if cfg == nil { + t.Fatal("expected a config, got nil") + } + if client.calledWithID != "p-1" { + t.Fatalf("expected client called with p-1, got %q", client.calledWithID) + } + if cfg.CPURequestTotal != "2" || cfg.CPULimitTotal != "4" { + t.Fatalf("cpu fields not mapped: %+v", cfg) + } + if cfg.MemoryRequestTotal != "4Gi" || cfg.MemoryLimitTotal != "8Gi" { + t.Fatalf("memory fields not mapped: %+v", cfg) + } + if cfg.EphemeralStorageTotal != "10Gi" { + t.Fatalf("ephemeral storage not mapped: %+v", cfg) + } + if cfg.PodCount != 10 || cfg.PVCCount != 5 { + t.Fatalf("count fields not mapped: %+v", cfg) + } + if cfg.ContainerCPURequestDefault != "100m" || cfg.ContainerCPULimitMax != "2" { + t.Fatalf("container cpu fields not mapped: %+v", cfg) + } + if cfg.ContainerMemoryRequestDefault != "128Mi" || cfg.ContainerMemoryLimitMax != "4Gi" { + t.Fatalf("container memory fields not mapped: %+v", cfg) + } + }) +} diff --git a/components/control-plane/internal/reconciler/reconciler.go b/components/control-plane/internal/reconciler/reconciler.go index 63c622c8..28477856 100644 --- a/components/control-plane/internal/reconciler/reconciler.go +++ b/components/control-plane/internal/reconciler/reconciler.go @@ -1394,6 +1394,24 @@ func (r *GatewayReconciler) Handle(ctx context.Context, event watcher.Event[*pb. return reconcileErr } + // Resolve the gateway's quota profile before provisioning. A fetch failure + // blocks provisioning (a profiled gateway must not run unconstrained); an + // empty profile_id yields nil, reconciling toward no quota (legacy). + quotaConfig, profileErr := r.resolveGatewayProfile(ctx, gw) + if profileErr != nil { + // A terminal failure (the profile is genuinely missing or unusable) means + // the gateway can never provision as declared, so mark it Failed rather + // than letting its quota go silently unenforced. A transient failure (the + // API server was momentarily unreachable) leaves the phase untouched to + // avoid flapping Failed->Provisioning on blips; returning reconcileErr + // still triggers a retry, which records the failure in the trace span. + if stderrors.Is(profileErr, errTerminalProfile) { + r.updateGatewayPhase(ctx, event.ResourceID, "Failed") + } + reconcileErr = fmt.Errorf("resolve gateway profile for gateway %s: %w", gw.Name, profileErr) + return reconcileErr + } + dnsNames := gw.ServerDnsNames if len(dnsNames) == 0 { dnsNames = []string{ @@ -1475,6 +1493,7 @@ func (r *GatewayReconciler) Handle(ctx context.Context, event watcher.Event[*pb. UpdateOIDC: r.makeOIDCUpdater(event.ResourceID), Exposure: r.exposure, RouteStillDesired: r.makeRouteStillDesired(event.ResourceID), + Quota: quotaConfig, } r.updateGatewayPhase(ctx, event.ResourceID, "Provisioning") @@ -1933,6 +1952,86 @@ func (r *GatewayReconciler) resolveDatabaseConfig(ctx context.Context, gw *pb.Ga } } +// errTerminalProfile marks a profile-resolution failure as terminal: the API +// server answered and the profile is genuinely missing or unusable, so a retry +// cannot succeed. Transient failures (a momentary API-server unavailability) are +// deliberately NOT wrapped with it, so the caller retries without flapping the +// gateway phase to Failed. Callers test membership with errors.Is. +var errTerminalProfile = stderrors.New("gateway profile terminally unresolvable") + +// resolveGatewayProfile fetches the gateway's GatewayProfile over gRPC and +// translates it into a QuotaConfig. An empty profile_id (legacy gateway) yields +// a nil config, which reconciles toward absence of quota. A fetch failure is +// returned as an error so the caller BLOCKS provisioning: a gateway that +// declares a profile must not run unconstrained. Terminal failures wrap +// errTerminalProfile (see its doc); transient failures do not. See +// openshell-gateway-quota.spec.md § Control Plane Quota Fetching. +func (r *GatewayReconciler) resolveGatewayProfile(ctx context.Context, gw *pb.Gateway) (*gateway.QuotaConfig, error) { + profileID := "" + if gw.ProfileId != nil { + profileID = *gw.ProfileId + } + return resolveGatewayProfileFromClient(ctx, profileID, pb.NewGatewayProfileServiceClient(r.grpcConn)) +} + +// resolveGatewayProfileFromClient is the testable core of resolveGatewayProfile: +// it takes the GatewayProfileServiceClient interface directly (rather than the +// concrete *grpc.ClientConn), so unit tests can inject a fake client. An empty +// profileID (legacy gateway) yields (nil, nil), reconciling toward absence of +// quota. A fetch failure or nil payload is returned as an error so the caller +// BLOCKS provisioning: a gateway that declares a profile must not run +// unconstrained. +func resolveGatewayProfileFromClient(ctx context.Context, profileID string, client pb.GatewayProfileServiceClient) (*gateway.QuotaConfig, error) { + if profileID == "" { + return nil, nil + } + + resp, err := client.GetGatewayProfile(ctx, &pb.GetGatewayProfileRequest{Id: profileID}) + if err != nil { + // NotFound is terminal: the API server answered and the profile does not + // exist, so retrying cannot succeed. Every other code (Unavailable, + // DeadlineExceeded, ...) is transient and left unwrapped so the caller + // retries without flapping the gateway phase to Failed. + if status.Code(err) == codes.NotFound { + return nil, fmt.Errorf("failed to fetch GatewayProfile %s: %w: %w", profileID, errTerminalProfile, err) + } + return nil, fmt.Errorf("failed to fetch GatewayProfile %s: %w", profileID, err) + } + p := resp.GatewayProfile + if p == nil { + // The API server responded but the profile is unusable: terminal. + return nil, fmt.Errorf("failed to fetch GatewayProfile %s: empty payload: %w", profileID, errTerminalProfile) + } + + return &gateway.QuotaConfig{ + CPURequestTotal: profileStr(p.CpuRequestTotal), + CPULimitTotal: profileStr(p.CpuLimitTotal), + MemoryRequestTotal: profileStr(p.MemoryRequestTotal), + MemoryLimitTotal: profileStr(p.MemoryLimitTotal), + EphemeralStorageTotal: profileStr(p.EphemeralStorageTotal), + PodCount: profileInt32(p.PodCount), + PVCCount: profileInt32(p.PvcCount), + ContainerCPURequestDefault: profileStr(p.ContainerCpuRequestDefault), + ContainerCPULimitMax: profileStr(p.ContainerCpuLimitMax), + ContainerMemoryRequestDefault: profileStr(p.ContainerMemoryRequestDefault), + ContainerMemoryLimitMax: profileStr(p.ContainerMemoryLimitMax), + }, nil +} + +func profileStr(s *string) string { + if s != nil { + return *s + } + return "" +} + +func profileInt32(i *int32) int32 { + if i != nil { + return *i + } + return 0 +} + func (r *GatewayReconciler) makeOIDCUpdater(gatewayID string) func(ctx context.Context, oidcJSON string) error { return func(ctx context.Context, oidcJSON string) error { client := pb.NewGatewayServiceClient(r.grpcConn) diff --git a/components/pr-test/e2e-openshell.sh b/components/pr-test/e2e-openshell.sh index 8d5d108b..0cab7344 100755 --- a/components/pr-test/e2e-openshell.sh +++ b/components/pr-test/e2e-openshell.sh @@ -723,6 +723,89 @@ os.chmod(os.path.join(config_dir, 'oidc_token.json'), 0o600) fi sep +# ── 9. namespace quota verification ────────────────────────────────────── + +echo "" +bold "9. Namespace Quota Verification" +echo "" + +if [[ -n "$GW_NAMESPACE" ]]; then + show_cmd "$CLI get resourcequota hypershell-gateway-quota -n $GW_NAMESPACE" + RQ_JSON=$($CLI get resourcequota hypershell-gateway-quota -n "$GW_NAMESPACE" -o json 2>/dev/null || true) + if [[ -n "$RQ_JSON" ]]; then + pass "ResourceQuota hypershell-gateway-quota present in ${GW_NAMESPACE}" + + CPU_LIMIT=$(echo "$RQ_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('spec',{}).get('hard',{}).get('limits.cpu',''))" 2>/dev/null || true) + MEM_LIMIT=$(echo "$RQ_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('spec',{}).get('hard',{}).get('limits.memory',''))" 2>/dev/null || true) + POD_COUNT=$(echo "$RQ_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('spec',{}).get('hard',{}).get('pods',''))" 2>/dev/null || true) + if [[ -n "$CPU_LIMIT" ]]; then + pass "ResourceQuota cpu limit: ${CPU_LIMIT}" + else + dim " - ResourceQuota cpu limit not set" + fi + if [[ -n "$MEM_LIMIT" ]]; then + pass "ResourceQuota memory limit: ${MEM_LIMIT}" + else + dim " - ResourceQuota memory limit not set" + fi + if [[ -n "$POD_COUNT" ]]; then + pass "ResourceQuota pod count: ${POD_COUNT}" + else + dim " - ResourceQuota pod count not set" + fi + else + fail_test "ResourceQuota hypershell-gateway-quota not found in ${GW_NAMESPACE}" + fi + + show_cmd "$CLI get limitrange hypershell-gateway-limits -n $GW_NAMESPACE" + LR_JSON=$($CLI get limitrange hypershell-gateway-limits -n "$GW_NAMESPACE" -o json 2>/dev/null || true) + if [[ -n "$LR_JSON" ]]; then + pass "LimitRange hypershell-gateway-limits present in ${GW_NAMESPACE}" + + CONTAINER_MAX_CPU=$(echo "$LR_JSON" | python3 -c " +import json,sys +d=json.load(sys.stdin) +for item in d.get('spec',{}).get('limits',[]): + if item.get('type') == 'Container': + print(item.get('max',{}).get('cpu','')) + break +" 2>/dev/null || true) + CONTAINER_MAX_MEM=$(echo "$LR_JSON" | python3 -c " +import json,sys +d=json.load(sys.stdin) +for item in d.get('spec',{}).get('limits',[]): + if item.get('type') == 'Container': + print(item.get('max',{}).get('memory','')) + break +" 2>/dev/null || true) + if [[ -n "$CONTAINER_MAX_CPU" ]]; then + pass "LimitRange container cpu max: ${CONTAINER_MAX_CPU}" + else + dim " - LimitRange container cpu max not set" + fi + if [[ -n "$CONTAINER_MAX_MEM" ]]; then + pass "LimitRange container memory max: ${CONTAINER_MAX_MEM}" + else + dim " - LimitRange container memory max not set" + fi + else + fail_test "LimitRange hypershell-gateway-limits not found in ${GW_NAMESPACE}" + fi + + # Verify the GatewayProfile API returns the default profile + show_cmd "curl ${API_URL}/api/hypershell/v1/gateway_profiles" + GP_RESP=$(curl -sk -H "${API_AUTH_HEADER:-}" "${API_URL}/api/hypershell/v1/gateway_profiles" 2>/dev/null || true) + GP_COUNT=$(echo "$GP_RESP" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('total',0))" 2>/dev/null || true) + if [[ "${GP_COUNT:-0}" -ge 1 ]]; then + pass "GatewayProfile API: ${GP_COUNT} profile(s) registered" + else + fail_test "GatewayProfile API returned no profiles (expected at least 1)" + fi +else + dim " - No gateway namespace recorded; skipping quota checks" +fi +sep + # ── results ─────────────────────────────────────────────────────────────── echo "" diff --git a/components/sdk-go/client/client.go b/components/sdk-go/client/client.go index 847fa5d5..d93ff852 100644 --- a/components/sdk-go/client/client.go +++ b/components/sdk-go/client/client.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package client diff --git a/components/sdk-go/client/gateway_api.go b/components/sdk-go/client/gateway_api.go index 00bb5525..724955b9 100644 --- a/components/sdk-go/client/gateway_api.go +++ b/components/sdk-go/client/gateway_api.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package client diff --git a/components/sdk-go/client/gateway_network_api.go b/components/sdk-go/client/gateway_network_api.go index 75d2411a..75407074 100644 --- a/components/sdk-go/client/gateway_network_api.go +++ b/components/sdk-go/client/gateway_network_api.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package client diff --git a/components/sdk-go/client/gateway_profile_api.go b/components/sdk-go/client/gateway_profile_api.go new file mode 100644 index 00000000..35c8c182 --- /dev/null +++ b/components/sdk-go/client/gateway_profile_api.go @@ -0,0 +1,75 @@ +// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. +// Source: components/api-server/openapi/openapi.yaml +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab + +package client + +import ( + "context" + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/openshift-online/hypershell/components/sdk-go/types" +) + +type GatewayProfileAPI struct { + client *Client +} + +func (c *Client) GatewayProfiles() *GatewayProfileAPI { + return &GatewayProfileAPI{client: c} +} + +func (a *GatewayProfileAPI) Create(ctx context.Context, resource *types.GatewayProfile) (*types.GatewayProfile, error) { + body, err := json.Marshal(resource) + if err != nil { + return nil, fmt.Errorf("marshal gateway_profile: %w", err) + } + var result types.GatewayProfile + if err := a.client.do(ctx, http.MethodPost, "/gateway_profiles", body, http.StatusCreated, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *GatewayProfileAPI) Get(ctx context.Context, id string) (*types.GatewayProfile, error) { + var result types.GatewayProfile + if err := a.client.do(ctx, http.MethodGet, "/gateway_profiles/"+url.PathEscape(id), nil, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *GatewayProfileAPI) List(ctx context.Context, opts *types.ListOptions) (*types.GatewayProfileList, error) { + var result types.GatewayProfileList + if err := a.client.doWithQuery(ctx, http.MethodGet, "/gateway_profiles", nil, http.StatusOK, &result, opts); err != nil { + return nil, err + } + return &result, nil +} + +func (a *GatewayProfileAPI) Update(ctx context.Context, id string, patch map[string]any) (*types.GatewayProfile, error) { + body, err := json.Marshal(patch) + if err != nil { + return nil, fmt.Errorf("marshal patch: %w", err) + } + var result types.GatewayProfile + if err := a.client.do(ctx, http.MethodPatch, "/gateway_profiles/"+url.PathEscape(id), body, http.StatusOK, &result); err != nil { + return nil, err + } + return &result, nil +} + +func (a *GatewayProfileAPI) Delete(ctx context.Context, id string) error { + return a.client.do(ctx, http.MethodDelete, "/gateway_profiles/"+url.PathEscape(id), nil, http.StatusNoContent, nil) +} + +func (a *GatewayProfileAPI) ListAll(ctx context.Context, opts *types.ListOptions) *Iterator[types.GatewayProfile] { + return NewIterator(func(page int) (*types.GatewayProfileList, error) { + o := *opts + o.Page = page + return a.List(ctx, &o) + }) +} diff --git a/components/sdk-go/client/gateway_release_api.go b/components/sdk-go/client/gateway_release_api.go index 72c64bf8..4fba8600 100644 --- a/components/sdk-go/client/gateway_release_api.go +++ b/components/sdk-go/client/gateway_release_api.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package client diff --git a/components/sdk-go/client/iterator.go b/components/sdk-go/client/iterator.go index a2792ca0..e535a738 100644 --- a/components/sdk-go/client/iterator.go +++ b/components/sdk-go/client/iterator.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package client diff --git a/components/sdk-go/client/managed_cluster_api.go b/components/sdk-go/client/managed_cluster_api.go index 646d6726..94c95f1d 100644 --- a/components/sdk-go/client/managed_cluster_api.go +++ b/components/sdk-go/client/managed_cluster_api.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package client diff --git a/components/sdk-go/client/managed_database_api.go b/components/sdk-go/client/managed_database_api.go index 6c86ed2c..3d9126b4 100644 --- a/components/sdk-go/client/managed_database_api.go +++ b/components/sdk-go/client/managed_database_api.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package client diff --git a/components/sdk-go/client/open_shell_gateway_service_account_api.go b/components/sdk-go/client/open_shell_gateway_service_account_api.go index 0cc42149..fcc25926 100644 --- a/components/sdk-go/client/open_shell_gateway_service_account_api.go +++ b/components/sdk-go/client/open_shell_gateway_service_account_api.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package client diff --git a/components/sdk-go/client/role_api.go b/components/sdk-go/client/role_api.go index 8cc27d75..c1d11a2e 100644 --- a/components/sdk-go/client/role_api.go +++ b/components/sdk-go/client/role_api.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package client diff --git a/components/sdk-go/client/role_binding_api.go b/components/sdk-go/client/role_binding_api.go index 76a4cd1f..86ad9831 100644 --- a/components/sdk-go/client/role_binding_api.go +++ b/components/sdk-go/client/role_binding_api.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package client diff --git a/components/sdk-go/types/base.go b/components/sdk-go/types/base.go index 33610fdb..3133655c 100644 --- a/components/sdk-go/types/base.go +++ b/components/sdk-go/types/base.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package types diff --git a/components/sdk-go/types/gateway.go b/components/sdk-go/types/gateway.go index 1541d672..2bd360f7 100644 --- a/components/sdk-go/types/gateway.go +++ b/components/sdk-go/types/gateway.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package types @@ -24,6 +24,7 @@ type Gateway struct { Namespace string `json:"namespace"` Oidc string `json:"oidc,omitempty"` Phase string `json:"phase,omitempty"` + ProfileID string `json:"profile_id,omitempty"` ReleaseID string `json:"release_id"` Route string `json:"route,omitempty"` RouteAddress string `json:"route_address,omitempty"` @@ -88,6 +89,11 @@ func (b *GatewayBuilder) Phase(v string) *GatewayBuilder { return b } +func (b *GatewayBuilder) ProfileID(v string) *GatewayBuilder { + b.resource.ProfileID = v + return b +} + func (b *GatewayBuilder) ReleaseID(v string) *GatewayBuilder { b.resource.ReleaseID = v return b @@ -193,6 +199,11 @@ func (b *GatewayPatchBuilder) Phase(v string) *GatewayPatchBuilder { return b } +func (b *GatewayPatchBuilder) ProfileID(v string) *GatewayPatchBuilder { + b.patch["profile_id"] = v + return b +} + func (b *GatewayPatchBuilder) ReleaseID(v string) *GatewayPatchBuilder { b.patch["release_id"] = v return b diff --git a/components/sdk-go/types/gateway_network.go b/components/sdk-go/types/gateway_network.go index c57107ce..2d9153c7 100644 --- a/components/sdk-go/types/gateway_network.go +++ b/components/sdk-go/types/gateway_network.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package types diff --git a/components/sdk-go/types/gateway_profile.go b/components/sdk-go/types/gateway_profile.go new file mode 100644 index 00000000..dd6628de --- /dev/null +++ b/components/sdk-go/types/gateway_profile.go @@ -0,0 +1,199 @@ +// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. +// Source: components/api-server/openapi/openapi.yaml +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab + +package types + +import ( + "errors" + "fmt" +) + +type GatewayProfile struct { + ObjectReference + + ContainerCpuLimitMax string `json:"container_cpu_limit_max,omitempty"` + ContainerCpuRequestDefault string `json:"container_cpu_request_default,omitempty"` + ContainerMemoryLimitMax string `json:"container_memory_limit_max,omitempty"` + ContainerMemoryRequestDefault string `json:"container_memory_request_default,omitempty"` + CpuLimitTotal string `json:"cpu_limit_total,omitempty"` + CpuRequestTotal string `json:"cpu_request_total,omitempty"` + Description string `json:"description,omitempty"` + EphemeralStorageTotal string `json:"ephemeral_storage_total,omitempty"` + MemoryLimitTotal string `json:"memory_limit_total,omitempty"` + MemoryRequestTotal string `json:"memory_request_total,omitempty"` + Name string `json:"name"` + PodCount int32 `json:"pod_count,omitempty"` + PvcCount int32 `json:"pvc_count,omitempty"` +} + +type GatewayProfileList struct { + ListMeta + Items []GatewayProfile `json:"items"` +} + +func (l *GatewayProfileList) GetItems() []GatewayProfile { return l.Items } +func (l *GatewayProfileList) GetTotal() int { return l.Total } +func (l *GatewayProfileList) GetPage() int { return l.Page } +func (l *GatewayProfileList) GetSize() int { return l.Size } + +type GatewayProfileBuilder struct { + resource GatewayProfile + errors []error +} + +func NewGatewayProfileBuilder() *GatewayProfileBuilder { + return &GatewayProfileBuilder{} +} + +func (b *GatewayProfileBuilder) ContainerCpuLimitMax(v string) *GatewayProfileBuilder { + b.resource.ContainerCpuLimitMax = v + return b +} + +func (b *GatewayProfileBuilder) ContainerCpuRequestDefault(v string) *GatewayProfileBuilder { + b.resource.ContainerCpuRequestDefault = v + return b +} + +func (b *GatewayProfileBuilder) ContainerMemoryLimitMax(v string) *GatewayProfileBuilder { + b.resource.ContainerMemoryLimitMax = v + return b +} + +func (b *GatewayProfileBuilder) ContainerMemoryRequestDefault(v string) *GatewayProfileBuilder { + b.resource.ContainerMemoryRequestDefault = v + return b +} + +func (b *GatewayProfileBuilder) CpuLimitTotal(v string) *GatewayProfileBuilder { + b.resource.CpuLimitTotal = v + return b +} + +func (b *GatewayProfileBuilder) CpuRequestTotal(v string) *GatewayProfileBuilder { + b.resource.CpuRequestTotal = v + return b +} + +func (b *GatewayProfileBuilder) Description(v string) *GatewayProfileBuilder { + b.resource.Description = v + return b +} + +func (b *GatewayProfileBuilder) EphemeralStorageTotal(v string) *GatewayProfileBuilder { + b.resource.EphemeralStorageTotal = v + return b +} + +func (b *GatewayProfileBuilder) MemoryLimitTotal(v string) *GatewayProfileBuilder { + b.resource.MemoryLimitTotal = v + return b +} + +func (b *GatewayProfileBuilder) MemoryRequestTotal(v string) *GatewayProfileBuilder { + b.resource.MemoryRequestTotal = v + return b +} + +func (b *GatewayProfileBuilder) Name(v string) *GatewayProfileBuilder { + b.resource.Name = v + return b +} + +func (b *GatewayProfileBuilder) PodCount(v int32) *GatewayProfileBuilder { + b.resource.PodCount = v + return b +} + +func (b *GatewayProfileBuilder) PvcCount(v int32) *GatewayProfileBuilder { + b.resource.PvcCount = v + return b +} + +func (b *GatewayProfileBuilder) Build() (*GatewayProfile, error) { + if b.resource.Name == "" { + b.errors = append(b.errors, fmt.Errorf("name is required")) + } + if len(b.errors) > 0 { + return nil, fmt.Errorf("validation failed: %w", errors.Join(b.errors...)) + } + return &b.resource, nil +} + +type GatewayProfilePatchBuilder struct { + patch map[string]any +} + +func NewGatewayProfilePatchBuilder() *GatewayProfilePatchBuilder { + return &GatewayProfilePatchBuilder{patch: make(map[string]any)} +} + +func (b *GatewayProfilePatchBuilder) ContainerCpuLimitMax(v string) *GatewayProfilePatchBuilder { + b.patch["container_cpu_limit_max"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) ContainerCpuRequestDefault(v string) *GatewayProfilePatchBuilder { + b.patch["container_cpu_request_default"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) ContainerMemoryLimitMax(v string) *GatewayProfilePatchBuilder { + b.patch["container_memory_limit_max"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) ContainerMemoryRequestDefault(v string) *GatewayProfilePatchBuilder { + b.patch["container_memory_request_default"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) CpuLimitTotal(v string) *GatewayProfilePatchBuilder { + b.patch["cpu_limit_total"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) CpuRequestTotal(v string) *GatewayProfilePatchBuilder { + b.patch["cpu_request_total"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) Description(v string) *GatewayProfilePatchBuilder { + b.patch["description"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) EphemeralStorageTotal(v string) *GatewayProfilePatchBuilder { + b.patch["ephemeral_storage_total"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) MemoryLimitTotal(v string) *GatewayProfilePatchBuilder { + b.patch["memory_limit_total"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) MemoryRequestTotal(v string) *GatewayProfilePatchBuilder { + b.patch["memory_request_total"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) Name(v string) *GatewayProfilePatchBuilder { + b.patch["name"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) PodCount(v int32) *GatewayProfilePatchBuilder { + b.patch["pod_count"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) PvcCount(v int32) *GatewayProfilePatchBuilder { + b.patch["pvc_count"] = v + return b +} + +func (b *GatewayProfilePatchBuilder) Build() map[string]any { + return b.patch +} diff --git a/components/sdk-go/types/gateway_release.go b/components/sdk-go/types/gateway_release.go index 9303e0de..342b3b7a 100644 --- a/components/sdk-go/types/gateway_release.go +++ b/components/sdk-go/types/gateway_release.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package types diff --git a/components/sdk-go/types/list_options.go b/components/sdk-go/types/list_options.go index 3e76f6cb..87362bac 100644 --- a/components/sdk-go/types/list_options.go +++ b/components/sdk-go/types/list_options.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package types diff --git a/components/sdk-go/types/managed_cluster.go b/components/sdk-go/types/managed_cluster.go index d85e3d70..aee6d500 100644 --- a/components/sdk-go/types/managed_cluster.go +++ b/components/sdk-go/types/managed_cluster.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package types @@ -13,8 +13,10 @@ type ManagedCluster struct { ObjectReference APIServerURL string `json:"api_server_url,omitempty"` + DatabaseID string `json:"database_id,omitempty"` KubeconfigSecret string `json:"kubeconfig_secret"` Name string `json:"name"` + ProfileID string `json:"profile_id,omitempty"` Provider string `json:"provider"` Region string `json:"region,omitempty"` Status string `json:"status,omitempty"` @@ -44,6 +46,11 @@ func (b *ManagedClusterBuilder) APIServerURL(v string) *ManagedClusterBuilder { return b } +func (b *ManagedClusterBuilder) DatabaseID(v string) *ManagedClusterBuilder { + b.resource.DatabaseID = v + return b +} + func (b *ManagedClusterBuilder) KubeconfigSecret(v string) *ManagedClusterBuilder { b.resource.KubeconfigSecret = v return b @@ -54,6 +61,11 @@ func (b *ManagedClusterBuilder) Name(v string) *ManagedClusterBuilder { return b } +func (b *ManagedClusterBuilder) ProfileID(v string) *ManagedClusterBuilder { + b.resource.ProfileID = v + return b +} + func (b *ManagedClusterBuilder) Provider(v string) *ManagedClusterBuilder { b.resource.Provider = v return b @@ -98,6 +110,11 @@ func (b *ManagedClusterPatchBuilder) APIServerURL(v string) *ManagedClusterPatch return b } +func (b *ManagedClusterPatchBuilder) DatabaseID(v string) *ManagedClusterPatchBuilder { + b.patch["database_id"] = v + return b +} + func (b *ManagedClusterPatchBuilder) KubeconfigSecret(v string) *ManagedClusterPatchBuilder { b.patch["kubeconfig_secret"] = v return b @@ -108,6 +125,11 @@ func (b *ManagedClusterPatchBuilder) Name(v string) *ManagedClusterPatchBuilder return b } +func (b *ManagedClusterPatchBuilder) ProfileID(v string) *ManagedClusterPatchBuilder { + b.patch["profile_id"] = v + return b +} + func (b *ManagedClusterPatchBuilder) Provider(v string) *ManagedClusterPatchBuilder { b.patch["provider"] = v return b diff --git a/components/sdk-go/types/managed_database.go b/components/sdk-go/types/managed_database.go index 8407ac9e..b2c05fae 100644 --- a/components/sdk-go/types/managed_database.go +++ b/components/sdk-go/types/managed_database.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package types diff --git a/components/sdk-go/types/open_shell_gateway_service_account.go b/components/sdk-go/types/open_shell_gateway_service_account.go index 5364c0d2..d644c608 100644 --- a/components/sdk-go/types/open_shell_gateway_service_account.go +++ b/components/sdk-go/types/open_shell_gateway_service_account.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package types diff --git a/components/sdk-go/types/role.go b/components/sdk-go/types/role.go index 10bca77a..04e66a83 100644 --- a/components/sdk-go/types/role.go +++ b/components/sdk-go/types/role.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package types diff --git a/components/sdk-go/types/role_binding.go b/components/sdk-go/types/role_binding.go index 5a863d2d..1a581672 100644 --- a/components/sdk-go/types/role_binding.go +++ b/components/sdk-go/types/role_binding.go @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab package types diff --git a/components/sdk-typescript/src/base.ts b/components/sdk-typescript/src/base.ts index 3049004e..01229399 100644 --- a/components/sdk-typescript/src/base.ts +++ b/components/sdk-typescript/src/base.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab export type ObjectReference = { id: string; diff --git a/components/sdk-typescript/src/client.ts b/components/sdk-typescript/src/client.ts index 3093afc8..f122c311 100644 --- a/components/sdk-typescript/src/client.ts +++ b/components/sdk-typescript/src/client.ts @@ -1,10 +1,11 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { SDKClientConfig } from './base.js'; import { GatewayAPI } from './gateway_api.js'; import { GatewayNetworkAPI } from './gateway_network_api.js'; +import { GatewayProfileAPI } from './gateway_profile_api.js'; import { GatewayReleaseAPI } from './gateway_release_api.js'; import { ManagedClusterAPI } from './managed_cluster_api.js'; import { ManagedDatabaseAPI } from './managed_database_api.js'; @@ -18,6 +19,7 @@ export class SDKClient { readonly gateways: GatewayAPI; readonly gatewayNetworks: GatewayNetworkAPI; + readonly gatewayProfiles: GatewayProfileAPI; readonly gatewayReleases: GatewayReleaseAPI; readonly managedClusters: ManagedClusterAPI; readonly managedDatabases: ManagedDatabaseAPI; @@ -37,6 +39,7 @@ export class SDKClient { this.gateways = new GatewayAPI(this.config); this.gatewayNetworks = new GatewayNetworkAPI(this.config); + this.gatewayProfiles = new GatewayProfileAPI(this.config); this.gatewayReleases = new GatewayReleaseAPI(this.config); this.managedClusters = new ManagedClusterAPI(this.config); this.managedDatabases = new ManagedDatabaseAPI(this.config); diff --git a/components/sdk-typescript/src/gateway.ts b/components/sdk-typescript/src/gateway.ts index de4e3184..0e2c99ce 100644 --- a/components/sdk-typescript/src/gateway.ts +++ b/components/sdk-typescript/src/gateway.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { ObjectReference, ListMeta } from './base.js'; @@ -17,6 +17,7 @@ export type Gateway = ObjectReference & { namespace: string; oidc: string; phase: string; + profile_id: string; release_id: string; route: string; route_address: string; @@ -39,6 +40,7 @@ export type GatewayCreateRequest = { image?: string; name: string; phase?: string; + profile_id?: string; release_id: string; route?: string; server_dns_names?: string; @@ -57,6 +59,7 @@ export type GatewayPatchRequest = { name?: string; oidc?: string; phase?: string; + profile_id?: string; release_id?: string; route?: string; route_address?: string; @@ -106,6 +109,11 @@ export class GatewayBuilder { return this; } + profileId(value: string): this { + this.data['profile_id'] = value; + return this; + } + releaseId(value: string): this { this.data['release_id'] = value; return this; @@ -205,6 +213,11 @@ export class GatewayPatchBuilder { return this; } + profileId(value: string): this { + this.data['profile_id'] = value; + return this; + } + releaseId(value: string): this { this.data['release_id'] = value; return this; diff --git a/components/sdk-typescript/src/gateway_api.ts b/components/sdk-typescript/src/gateway_api.ts index 4e71256d..2befdc2a 100644 --- a/components/sdk-typescript/src/gateway_api.ts +++ b/components/sdk-typescript/src/gateway_api.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js'; import { sdkFetch, buildQueryString } from './base.js'; diff --git a/components/sdk-typescript/src/gateway_network.ts b/components/sdk-typescript/src/gateway_network.ts index 97fe414a..530b89af 100644 --- a/components/sdk-typescript/src/gateway_network.ts +++ b/components/sdk-typescript/src/gateway_network.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { ObjectReference, ListMeta } from './base.js'; diff --git a/components/sdk-typescript/src/gateway_network_api.ts b/components/sdk-typescript/src/gateway_network_api.ts index d08e74a2..5edc1afb 100644 --- a/components/sdk-typescript/src/gateway_network_api.ts +++ b/components/sdk-typescript/src/gateway_network_api.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js'; import { sdkFetch, buildQueryString } from './base.js'; diff --git a/components/sdk-typescript/src/gateway_profile.ts b/components/sdk-typescript/src/gateway_profile.ts new file mode 100644 index 00000000..fb7ba3b8 --- /dev/null +++ b/components/sdk-typescript/src/gateway_profile.ts @@ -0,0 +1,208 @@ +// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. +// Source: components/api-server/openapi/openapi.yaml +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab + +import type { ObjectReference, ListMeta } from './base.js'; + +export type GatewayProfile = ObjectReference & { + container_cpu_limit_max: string; + container_cpu_request_default: string; + container_memory_limit_max: string; + container_memory_request_default: string; + cpu_limit_total: string; + cpu_request_total: string; + description: string; + ephemeral_storage_total: string; + memory_limit_total: string; + memory_request_total: string; + name: string; + pod_count: number; + pvc_count: number; +}; + +export type GatewayProfileList = ListMeta & { + items: GatewayProfile[]; +}; + +export type GatewayProfileCreateRequest = { + container_cpu_limit_max?: string; + container_cpu_request_default?: string; + container_memory_limit_max?: string; + container_memory_request_default?: string; + cpu_limit_total?: string; + cpu_request_total?: string; + description?: string; + ephemeral_storage_total?: string; + memory_limit_total?: string; + memory_request_total?: string; + name: string; + pod_count?: number; + pvc_count?: number; +}; + +export type GatewayProfilePatchRequest = { + container_cpu_limit_max?: string; + container_cpu_request_default?: string; + container_memory_limit_max?: string; + container_memory_request_default?: string; + cpu_limit_total?: string; + cpu_request_total?: string; + description?: string; + ephemeral_storage_total?: string; + memory_limit_total?: string; + memory_request_total?: string; + name?: string; + pod_count?: number; + pvc_count?: number; +}; + +export class GatewayProfileBuilder { + private data: Record = {}; + + + containerCpuLimitMax(value: string): this { + this.data['container_cpu_limit_max'] = value; + return this; + } + + containerCpuRequestDefault(value: string): this { + this.data['container_cpu_request_default'] = value; + return this; + } + + containerMemoryLimitMax(value: string): this { + this.data['container_memory_limit_max'] = value; + return this; + } + + containerMemoryRequestDefault(value: string): this { + this.data['container_memory_request_default'] = value; + return this; + } + + cpuLimitTotal(value: string): this { + this.data['cpu_limit_total'] = value; + return this; + } + + cpuRequestTotal(value: string): this { + this.data['cpu_request_total'] = value; + return this; + } + + description(value: string): this { + this.data['description'] = value; + return this; + } + + ephemeralStorageTotal(value: string): this { + this.data['ephemeral_storage_total'] = value; + return this; + } + + memoryLimitTotal(value: string): this { + this.data['memory_limit_total'] = value; + return this; + } + + memoryRequestTotal(value: string): this { + this.data['memory_request_total'] = value; + return this; + } + + name(value: string): this { + this.data['name'] = value; + return this; + } + + podCount(value: number): this { + this.data['pod_count'] = value; + return this; + } + + pvcCount(value: number): this { + this.data['pvc_count'] = value; + return this; + } + + build(): GatewayProfileCreateRequest { + if (!this.data['name']) { + throw new Error('name is required'); + } + return this.data as GatewayProfileCreateRequest; + } +} + +export class GatewayProfilePatchBuilder { + private data: Record = {}; + + + containerCpuLimitMax(value: string): this { + this.data['container_cpu_limit_max'] = value; + return this; + } + + containerCpuRequestDefault(value: string): this { + this.data['container_cpu_request_default'] = value; + return this; + } + + containerMemoryLimitMax(value: string): this { + this.data['container_memory_limit_max'] = value; + return this; + } + + containerMemoryRequestDefault(value: string): this { + this.data['container_memory_request_default'] = value; + return this; + } + + cpuLimitTotal(value: string): this { + this.data['cpu_limit_total'] = value; + return this; + } + + cpuRequestTotal(value: string): this { + this.data['cpu_request_total'] = value; + return this; + } + + description(value: string): this { + this.data['description'] = value; + return this; + } + + ephemeralStorageTotal(value: string): this { + this.data['ephemeral_storage_total'] = value; + return this; + } + + memoryLimitTotal(value: string): this { + this.data['memory_limit_total'] = value; + return this; + } + + memoryRequestTotal(value: string): this { + this.data['memory_request_total'] = value; + return this; + } + + name(value: string): this { + this.data['name'] = value; + return this; + } + + podCount(value: number): this { + this.data['pod_count'] = value; + return this; + } + + pvcCount(value: number): this { + this.data['pvc_count'] = value; + return this; + } + + build(): GatewayProfilePatchRequest { + return this.data as GatewayProfilePatchRequest; + } +} diff --git a/components/sdk-typescript/src/gateway_profile_api.ts b/components/sdk-typescript/src/gateway_profile_api.ts new file mode 100644 index 00000000..0cdf93b8 --- /dev/null +++ b/components/sdk-typescript/src/gateway_profile_api.ts @@ -0,0 +1,48 @@ +// Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. +// Source: components/api-server/openapi/openapi.yaml +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab + +import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js'; +import { sdkFetch, buildQueryString } from './base.js'; +import type { GatewayProfile, GatewayProfileList, GatewayProfileCreateRequest, GatewayProfilePatchRequest } from './gateway_profile.js'; + +export class GatewayProfileAPI { + constructor(private readonly config: SDKClientConfig) {} + + async create(data: GatewayProfileCreateRequest, opts?: RequestOptions): Promise { + return sdkFetch(this.config, 'POST', '/gateway_profiles', data, opts); + } + + async get(id: string, opts?: RequestOptions): Promise { + return sdkFetch(this.config, 'GET', `/gateway_profiles/${id}`, undefined, opts); + } + + async list(listOpts?: ListOptions, opts?: RequestOptions): Promise { + const qs = buildQueryString(listOpts); + return sdkFetch(this.config, 'GET', `/gateway_profiles${qs}`, undefined, opts); + } + + async update(id: string, patch: GatewayProfilePatchRequest, opts?: RequestOptions): Promise { + return sdkFetch(this.config, 'PATCH', `/gateway_profiles/${id}`, patch, opts); + } + + + async delete(id: string, opts?: RequestOptions): Promise { + return sdkFetch(this.config, 'DELETE', `/gateway_profiles/${id}`, undefined, opts); + } + + + async *listAll(size: number = 100, opts?: RequestOptions): AsyncGenerator { + let page = 1; + while (true) { + const result = await this.list({ page, size }, opts); + for (const item of result.items) { + yield item; + } + if (page * size >= result.total) { + break; + } + page++; + } + } +} diff --git a/components/sdk-typescript/src/gateway_release.ts b/components/sdk-typescript/src/gateway_release.ts index c915a33f..be75d2f5 100644 --- a/components/sdk-typescript/src/gateway_release.ts +++ b/components/sdk-typescript/src/gateway_release.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { ObjectReference, ListMeta } from './base.js'; diff --git a/components/sdk-typescript/src/gateway_release_api.ts b/components/sdk-typescript/src/gateway_release_api.ts index f990e48a..f3e970fc 100644 --- a/components/sdk-typescript/src/gateway_release_api.ts +++ b/components/sdk-typescript/src/gateway_release_api.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js'; import { sdkFetch, buildQueryString } from './base.js'; diff --git a/components/sdk-typescript/src/index.ts b/components/sdk-typescript/src/index.ts index ee5fcee8..8868b278 100644 --- a/components/sdk-typescript/src/index.ts +++ b/components/sdk-typescript/src/index.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab export { SDKClient } from './client.js'; export type { SDKClientConfig, ListOptions, RequestOptions, ObjectReference, ListMeta, APIError } from './base.js'; @@ -19,6 +19,12 @@ export { GatewayNetworkAPI } from './gateway_network_api.js'; +export type { GatewayProfile, GatewayProfileList, GatewayProfileCreateRequest, GatewayProfilePatchRequest } from './gateway_profile.js'; +export { GatewayProfileBuilder, GatewayProfilePatchBuilder } from './gateway_profile.js'; +export { GatewayProfileAPI } from './gateway_profile_api.js'; + + + export type { GatewayRelease, GatewayReleaseList, GatewayReleaseCreateRequest, GatewayReleasePatchRequest } from './gateway_release.js'; export { GatewayReleaseBuilder, GatewayReleasePatchBuilder } from './gateway_release.js'; export { GatewayReleaseAPI } from './gateway_release_api.js'; diff --git a/components/sdk-typescript/src/managed_cluster.ts b/components/sdk-typescript/src/managed_cluster.ts index a648937d..5611de48 100644 --- a/components/sdk-typescript/src/managed_cluster.ts +++ b/components/sdk-typescript/src/managed_cluster.ts @@ -1,13 +1,15 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { ObjectReference, ListMeta } from './base.js'; export type ManagedCluster = ObjectReference & { api_server_url: string; + database_id: string; kubeconfig_secret: string; name: string; + profile_id: string; provider: string; region: string; status: string; @@ -19,8 +21,10 @@ export type ManagedClusterList = ListMeta & { export type ManagedClusterCreateRequest = { api_server_url?: string; + database_id?: string; kubeconfig_secret: string; name: string; + profile_id?: string; provider: string; region?: string; status?: string; @@ -28,8 +32,10 @@ export type ManagedClusterCreateRequest = { export type ManagedClusterPatchRequest = { api_server_url?: string; + database_id?: string; kubeconfig_secret?: string; name?: string; + profile_id?: string; provider?: string; region?: string; status?: string; @@ -44,6 +50,11 @@ export class ManagedClusterBuilder { return this; } + databaseId(value: string): this { + this.data['database_id'] = value; + return this; + } + kubeconfigSecret(value: string): this { this.data['kubeconfig_secret'] = value; return this; @@ -54,6 +65,11 @@ export class ManagedClusterBuilder { return this; } + profileId(value: string): this { + this.data['profile_id'] = value; + return this; + } + provider(value: string): this { this.data['provider'] = value; return this; @@ -92,6 +108,11 @@ export class ManagedClusterPatchBuilder { return this; } + databaseId(value: string): this { + this.data['database_id'] = value; + return this; + } + kubeconfigSecret(value: string): this { this.data['kubeconfig_secret'] = value; return this; @@ -102,6 +123,11 @@ export class ManagedClusterPatchBuilder { return this; } + profileId(value: string): this { + this.data['profile_id'] = value; + return this; + } + provider(value: string): this { this.data['provider'] = value; return this; diff --git a/components/sdk-typescript/src/managed_cluster_api.ts b/components/sdk-typescript/src/managed_cluster_api.ts index f83d1b15..3188dd0b 100644 --- a/components/sdk-typescript/src/managed_cluster_api.ts +++ b/components/sdk-typescript/src/managed_cluster_api.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js'; import { sdkFetch, buildQueryString } from './base.js'; diff --git a/components/sdk-typescript/src/managed_database.ts b/components/sdk-typescript/src/managed_database.ts index a156b437..92d826fc 100644 --- a/components/sdk-typescript/src/managed_database.ts +++ b/components/sdk-typescript/src/managed_database.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { ObjectReference, ListMeta } from './base.js'; diff --git a/components/sdk-typescript/src/managed_database_api.ts b/components/sdk-typescript/src/managed_database_api.ts index e3b31217..f3589a1b 100644 --- a/components/sdk-typescript/src/managed_database_api.ts +++ b/components/sdk-typescript/src/managed_database_api.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js'; import { sdkFetch, buildQueryString } from './base.js'; diff --git a/components/sdk-typescript/src/open_shell_gateway_service_account.ts b/components/sdk-typescript/src/open_shell_gateway_service_account.ts index fdb26eb3..2f67788f 100644 --- a/components/sdk-typescript/src/open_shell_gateway_service_account.ts +++ b/components/sdk-typescript/src/open_shell_gateway_service_account.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab export type OpenShellGatewayServiceAccountCapabilities = { diff --git a/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts b/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts index 3e2d5f93..78450b4e 100644 --- a/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts +++ b/components/sdk-typescript/src/open_shell_gateway_service_account_api.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { SDKClientConfig, RequestOptions } from './base.js'; import { sdkFetch } from './base.js'; diff --git a/components/sdk-typescript/src/role.ts b/components/sdk-typescript/src/role.ts index f433f5cf..f51176a0 100644 --- a/components/sdk-typescript/src/role.ts +++ b/components/sdk-typescript/src/role.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { ObjectReference, ListMeta } from './base.js'; diff --git a/components/sdk-typescript/src/role_api.ts b/components/sdk-typescript/src/role_api.ts index 1ba0e062..f733563a 100644 --- a/components/sdk-typescript/src/role_api.ts +++ b/components/sdk-typescript/src/role_api.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js'; import { sdkFetch, buildQueryString } from './base.js'; diff --git a/components/sdk-typescript/src/role_binding.ts b/components/sdk-typescript/src/role_binding.ts index d995b20d..ec179b64 100644 --- a/components/sdk-typescript/src/role_binding.ts +++ b/components/sdk-typescript/src/role_binding.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { ObjectReference, ListMeta } from './base.js'; diff --git a/components/sdk-typescript/src/role_binding_api.ts b/components/sdk-typescript/src/role_binding_api.ts index 9678dcbc..9c7eed5d 100644 --- a/components/sdk-typescript/src/role_binding_api.ts +++ b/components/sdk-typescript/src/role_binding_api.ts @@ -1,6 +1,6 @@ // Code generated by trex-sdk-generator from openapi.yaml - DO NOT EDIT. // Source: components/api-server/openapi/openapi.yaml -// Spec SHA256: 3a4b85a8fe6c5a35061a7a9a470d9813d217c510e482871cd237ebf7bd70f446 +// Spec SHA256: c680d033b0f6ae56d5d8a6b7d3deed318b1c775ebea67a5d637d75a87caa6dab import type { SDKClientConfig, ListOptions, RequestOptions } from './base.js'; import { sdkFetch, buildQueryString } from './base.js'; diff --git a/components/web-console/app/adapters/api/gateway-operations.test.ts b/components/web-console/app/adapters/api/gateway-operations.test.ts index 1381cf95..231d6060 100644 --- a/components/web-console/app/adapters/api/gateway-operations.test.ts +++ b/components/web-console/app/adapters/api/gateway-operations.test.ts @@ -3,6 +3,8 @@ import { SDKAPIError, type Gateway, type GatewayList, + type GatewayProfile, + type GatewayProfileList, type ManagedCluster, type ManagedClusterList, type OpenShellGatewayServiceAccountCreateResponse, @@ -20,6 +22,9 @@ const gatewayApi = { list: vi.fn(), update: vi.fn(), }; +const gatewayProfileApi = { + list: vi.fn(), +}; const managedClusterApi = { get: vi.fn(), list: vi.fn(), @@ -32,6 +37,7 @@ const serviceAccountApi = { revoke: vi.fn(), }; const gatewayApiFactory = vi.fn(() => ({ + gatewayProfiles: gatewayProfileApi, gateways: gatewayApi, managedClusters: managedClusterApi, openShellGatewayServiceAccounts: serviceAccountApi, @@ -66,6 +72,7 @@ function gateway(overrides: Partial = {}): Gateway { namespace: "openshell", oidc: "", phase: "", + profile_id: "", release_id: "release-1", route: "", route_address: "", @@ -93,17 +100,58 @@ function gatewayList( }; } +function gatewayProfile( + overrides: Partial = {}, +): GatewayProfile { + return { + container_cpu_limit_max: "500m", + container_cpu_request_default: "100m", + container_memory_limit_max: "512Mi", + container_memory_request_default: "128Mi", + cpu_limit_total: "8", + cpu_request_total: "4", + created_at: "2026-08-10T14:30:00Z", + description: "Small resource quota", + ephemeral_storage_total: "10Gi", + href: "/api/hypershell/v1/gateway_profiles/profile-small", + id: "profile-small", + kind: "GatewayProfile", + memory_limit_total: "16Gi", + memory_request_total: "8Gi", + name: "Small profile", + pod_count: 10, + pvc_count: 5, + updated_at: null, + ...overrides, + }; +} + +function gatewayProfileList( + items: GatewayProfile[], + total = items.length, +): GatewayProfileList { + return { + items, + kind: "GatewayProfileList", + page: 1, + size: items.length, + total, + }; +} + function managedCluster( overrides: Partial = {}, ): ManagedCluster { return { api_server_url: "https://api.east.example.com", created_at: null, + database_id: "", href: "/api/hypershell/v1/managed_clusters/cluster-east", id: "cluster-east", kind: "ManagedCluster", kubeconfig_secret: "cluster-east-kubeconfig", name: "Cluster East", + profile_id: "profile-small", provider: "AWS", region: "us-east-1", status: "Ready", @@ -191,6 +239,7 @@ describe("gateway API operations adapter", () => { { id: "cluster-east", name: "Cluster East", + profileId: "profile-small", provider: "AWS", region: "us-east-1", status: "Ready", @@ -209,6 +258,61 @@ describe("gateway API operations adapter", () => { ); }); + it("maps one authoritative gateway-profile search page into summaries", async () => { + const abortController = new AbortController(); + gatewayProfileApi.list.mockResolvedValue( + gatewayProfileList([gatewayProfile()]), + ); + + await expect( + controlPlane.findGatewayProfiles(" small ", { + ...context, + signal: abortController.signal, + }), + ).resolves.toEqual({ + hasMore: false, + items: [ + { + description: "Small resource quota", + id: "profile-small", + name: "Small profile", + }, + ], + }); + expect(gatewayProfileApi.list).toHaveBeenCalledOnce(); + expect(gatewayProfileApi.list).toHaveBeenCalledWith( + { + orderBy: "name asc", + page: 1, + search: "name ilike '%small%'", + size: 20, + }, + { signal: abortController.signal }, + ); + }); + + it("reports when a bounded gateway-profile search has more results", async () => { + gatewayProfileApi.list.mockResolvedValue( + gatewayProfileList( + Array.from({ length: 20 }, (_, index) => + gatewayProfile({ + id: `profile-${String(index)}`, + name: `Profile ${String(index)}`, + }), + ), + 21, + ), + ); + + await expect( + controlPlane.findGatewayProfiles("", context), + ).resolves.toMatchObject({ hasMore: true }); + expect(gatewayProfileApi.list).toHaveBeenCalledWith( + { orderBy: "name asc", page: 1, size: 20 }, + { signal: undefined }, + ); + }); + it("maps one gateway-scoped service-account page and preserves literal search", async () => { serviceAccountApi.list.mockResolvedValue( serviceAccountList([serviceAccount()]), @@ -652,6 +756,43 @@ describe("gateway API operations adapter", () => { ); }); + it("provisions with the selected gateway profile when one is chosen", async () => { + gatewayApi.create.mockResolvedValue( + gateway({ database_id: "", profile_id: "profile-small", release_id: "" }), + ); + + await expect( + controlPlane.provisionGateway( + { + clusterId: "cluster-east", + name: "team-gateway", + profileId: "profile-small", + }, + context, + ), + ).resolves.toMatchObject({ profileId: "profile-small" }); + + expect(gatewayApi.create).toHaveBeenCalledWith( + { + cluster_id: "cluster-east", + database_id: "", + name: "team-gateway", + profile_id: "profile-small", + release_id: "", + route: '{"enabled":true}', + }, + { signal: undefined }, + ); + }); + + it("leaves the gateway profile unavailable when none is assigned", async () => { + gatewayApi.get.mockResolvedValue(gateway({ profile_id: "" })); + + const result = await controlPlane.getGateway("gateway-1", context); + + expect(result.profileId).toBeUndefined(); + }); + it("maps detail, rename, and deletion operations", async () => { gatewayApi.get.mockResolvedValue(gateway()); gatewayApi.update.mockResolvedValue(gateway({ name: "Renamed gateway" })); diff --git a/components/web-console/app/adapters/api/gateway-operations.ts b/components/web-console/app/adapters/api/gateway-operations.ts index 2405be71..1b03aa5d 100644 --- a/components/web-console/app/adapters/api/gateway-operations.ts +++ b/components/web-console/app/adapters/api/gateway-operations.ts @@ -5,6 +5,7 @@ import type { GatewayInvocationContext, GatewayListRequest, GatewayPlacement, + GatewayProfileSummary, GatewayRecord, OpenShellGatewayServiceAccountCapabilities, OpenShellGatewayServiceAccountConnection, @@ -19,6 +20,7 @@ import { import { SDKAPIError, type Gateway, + type GatewayProfile, type ManagedCluster, type OpenShellGatewayServiceAccountCapabilities as ApiServiceAccountCapabilities, type OpenShellGatewayServiceAccountConnection as ApiServiceAccountConnection, @@ -33,12 +35,14 @@ type GatewayApi = Pick< SDKClient["gateways"], "create" | "delete" | "get" | "list" | "update" >; +type GatewayProfileApi = Pick; type ManagedClusterApi = Pick; type ServiceAccountApi = Pick< SDKClient["openShellGatewayServiceAccounts"], "create" | "delete" | "get" | "list" | "revoke" >; interface GatewayApiClient { + gatewayProfiles: GatewayProfileApi; gateways: GatewayApi; managedClusters: ManagedClusterApi; openShellGatewayServiceAccounts: ServiceAccountApi; @@ -132,11 +136,23 @@ function toGatewayRecord(gateway: Gateway): GatewayRecord { ...(oidcClientId ? { oidcClientId } : {}), ...(oidcIssuer ? { oidcIssuer } : {}), phase: gateway.phase, + ...(gateway.profile_id ? { profileId: gateway.profile_id } : {}), releaseId: gateway.release_id, status: gateway.status, }; } +function toGatewayProfileSummary( + profile: GatewayProfile, +): GatewayProfileSummary { + const description = optionalString(profile.description); + return { + ...(description ? { description } : {}), + id: profile.id, + name: profile.name, + }; +} + function optionalString(value: unknown): string { return typeof value === "string" ? value.trim() : ""; } @@ -217,10 +233,12 @@ function optionalNumber(value: unknown): number | undefined { function toGatewayPlacement(cluster: ManagedCluster): GatewayPlacement { const region = optionalString(cluster.region); const status = optionalString(cluster.status); + const profileId = optionalString(cluster.profile_id); return { id: cluster.id, name: cluster.name, provider: cluster.provider, + ...(profileId ? { profileId } : {}), ...(region ? { region } : {}), ...(status ? { status } : {}), }; @@ -336,6 +354,36 @@ export function createGatewayControlPlaneAdapter( }; }); }, + async findGatewayProfiles(search, context) { + return mapFailure(async () => { + const normalizedSearch = search.trim(); + const literal = escapeIlikeLiteral(normalizedSearch); + const result = await apiClient( + apiFactory, + context, + ).gatewayProfiles.list( + { + orderBy: "name asc", + page: 1, + ...(literal ? { search: `name ilike '%${literal}%'` } : {}), + size: placementPageSize, + }, + { signal: context.signal }, + ); + const expectedItemCount = Math.min(placementPageSize, result.total); + if ( + result.page !== 1 || + result.total < 0 || + result.items.length !== expectedItemCount + ) { + throw new GatewayOperationError("unavailable"); + } + return { + hasMore: result.total > result.items.length, + items: result.items.map(toGatewayProfileSummary), + }; + }); + }, async getGatewayPlacement(clusterId, context) { return mapFailure(async () => toGatewayPlacement( @@ -489,6 +537,9 @@ export function createGatewayControlPlaneAdapter( cluster_id: input.clusterId, database_id: "", name: input.name, + ...(input.profileId === undefined + ? {} + : { profile_id: input.profileId }), release_id: "", route: JSON.stringify({ enabled: true }), }, diff --git a/components/web-console/app/adapters/api/gateway-profile-operations.test.ts b/components/web-console/app/adapters/api/gateway-profile-operations.test.ts new file mode 100644 index 00000000..406a4a69 --- /dev/null +++ b/components/web-console/app/adapters/api/gateway-profile-operations.test.ts @@ -0,0 +1,250 @@ +import { GatewayProfileOperationError } from "@openshift-online/hypershell-gateway-management-ui"; +import { + SDKAPIError, + type GatewayProfile, + type GatewayProfileList, +} from "@openshift-online/hypershell-sdk"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { createGatewayProfileControlPlaneAdapter } from "./gateway-profile-operations"; + +const gatewayProfileApi = { + create: vi.fn(), + delete: vi.fn(), + get: vi.fn(), + list: vi.fn(), +}; +const gatewayProfileApiFactory = vi.fn(() => ({ + gatewayProfiles: gatewayProfileApi, +})); +const controlPlane = createGatewayProfileControlPlaneAdapter( + gatewayProfileApiFactory, +); +const context = { + correlationId: "11111111-1111-4111-8111-111111111111", +}; +const listRequest = { + page: 2, + search: "small's profile", + size: 20, + sortDirection: "desc", + sortField: "created", +} as const; + +function gatewayProfile( + overrides: Partial = {}, +): GatewayProfile { + return { + container_cpu_limit_max: "500m", + container_cpu_request_default: "100m", + container_memory_limit_max: "512Mi", + container_memory_request_default: "128Mi", + cpu_limit_total: "8", + cpu_request_total: "4", + created_at: "2026-08-10T14:30:00Z", + description: "Small resource quota", + ephemeral_storage_total: "10Gi", + href: "/api/hypershell/v1/gateway_profiles/profile-small", + id: "profile-small", + kind: "GatewayProfile", + memory_limit_total: "16Gi", + memory_request_total: "8Gi", + name: "Small profile", + pod_count: 10, + pvc_count: 5, + updated_at: null, + ...overrides, + }; +} + +function gatewayProfileList( + items: GatewayProfile[], + total = items.length, + page = 1, +): GatewayProfileList { + return { + items, + kind: "GatewayProfileList", + page, + size: items.length, + total, + }; +} + +function apiError(statusCode: number, code = "conflict"): SDKAPIError { + return new SDKAPIError({ + code, + href: "", + id: "", + kind: "Error", + operation_id: "operation-1", + reason: "boom", + status_code: statusCode, + }); +} + +describe("gateway profile control plane adapter", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("threads the correlation identifier through the API factory", async () => { + gatewayProfileApi.get.mockResolvedValue(gatewayProfile()); + + await controlPlane.getGatewayProfile("profile-small", context); + + expect(gatewayProfileApiFactory).toHaveBeenCalledWith( + "11111111-1111-4111-8111-111111111111", + ); + expect(gatewayProfileApi.get).toHaveBeenCalledWith("profile-small", { + signal: undefined, + }); + }); + + it("maps a gateway profile record from the API model", async () => { + gatewayProfileApi.get.mockResolvedValue(gatewayProfile()); + + const record = await controlPlane.getGatewayProfile( + "profile-small", + context, + ); + + expect(record).toEqual({ + containerCpuLimitMax: "500m", + containerCpuRequestDefault: "100m", + containerMemoryLimitMax: "512Mi", + containerMemoryRequestDefault: "128Mi", + cpuLimitTotal: "8", + cpuRequestTotal: "4", + createdAt: "2026-08-10T14:30:00Z", + description: "Small resource quota", + ephemeralStorageTotal: "10Gi", + id: "profile-small", + memoryLimitTotal: "16Gi", + memoryRequestTotal: "8Gi", + name: "Small profile", + podCount: 10, + pvcCount: 5, + }); + }); + + it("omits empty optional quota fields", async () => { + gatewayProfileApi.get.mockResolvedValue( + gatewayProfile({ + container_cpu_limit_max: "", + cpu_request_total: "", + description: "", + }), + ); + + const record = await controlPlane.getGatewayProfile( + "profile-small", + context, + ); + + expect(record.containerCpuLimitMax).toBeUndefined(); + expect(record.cpuRequestTotal).toBeUndefined(); + expect(record.description).toBeUndefined(); + }); + + it("creates a gateway profile from defined fields only", async () => { + gatewayProfileApi.create.mockResolvedValue(gatewayProfile()); + + await controlPlane.createGatewayProfile( + { cpuRequestTotal: "4", name: "Small profile", podCount: 10 }, + context, + ); + + expect(gatewayProfileApi.create).toHaveBeenCalledWith( + { cpu_request_total: "4", name: "Small profile", pod_count: 10 }, + { signal: undefined }, + ); + }); + + it("lists gateway profiles with an escaped search and sort", async () => { + gatewayProfileApi.list.mockResolvedValue( + gatewayProfileList([gatewayProfile()], 21, 2), + ); + + const result = await controlPlane.listGatewayProfiles(listRequest, context); + + expect(gatewayProfileApi.list).toHaveBeenCalledWith( + { + orderBy: "created_at desc", + page: 2, + search: "name ilike '%small''s profile%'", + size: 20, + }, + { signal: undefined }, + ); + expect(result).toEqual({ + items: [expect.objectContaining({ id: "profile-small" })], + page: 2, + size: 20, + total: 21, + }); + }); + + it("omits the search filter when the query is blank", async () => { + gatewayProfileApi.list.mockResolvedValue(gatewayProfileList([], 0)); + + await controlPlane.listGatewayProfiles( + { ...listRequest, page: 1, search: " " }, + context, + ); + + expect(gatewayProfileApi.list).toHaveBeenCalledWith( + { orderBy: "created_at desc", page: 1, size: 20 }, + { signal: undefined }, + ); + }); + + it("rejects an inconsistent list page", async () => { + gatewayProfileApi.list.mockResolvedValue( + gatewayProfileList([gatewayProfile()], 0, 2), + ); + + await expect( + controlPlane.listGatewayProfiles(listRequest, context), + ).rejects.toThrow(GatewayProfileOperationError); + }); + + it("deletes a gateway profile", async () => { + gatewayProfileApi.delete.mockResolvedValue(undefined); + + await controlPlane.removeGatewayProfile("profile-small", context); + + expect(gatewayProfileApi.delete).toHaveBeenCalledWith("profile-small", { + signal: undefined, + }); + }); + + it.each([ + [403, "denied"], + [404, "not-found"], + [409, "conflict"], + [503, "unavailable"], + [418, "unknown"], + ] as const)("maps HTTP %s to the %s failure kind", async (status, kind) => { + gatewayProfileApi.delete.mockRejectedValue(apiError(status)); + + const error = await controlPlane + .removeGatewayProfile("profile-small", context) + .catch((thrown: unknown) => thrown); + + expect(error).toBeInstanceOf(GatewayProfileOperationError); + expect((error as GatewayProfileOperationError).kind).toBe(kind); + expect((error as GatewayProfileOperationError).operationId).toBe( + "operation-1", + ); + }); + + it("propagates non-API errors unchanged", async () => { + const failure = new Error("network down"); + gatewayProfileApi.get.mockRejectedValue(failure); + + await expect( + controlPlane.getGatewayProfile("profile-small", context), + ).rejects.toBe(failure); + }); +}); diff --git a/components/web-console/app/adapters/api/gateway-profile-operations.ts b/components/web-console/app/adapters/api/gateway-profile-operations.ts new file mode 100644 index 00000000..5473b436 --- /dev/null +++ b/components/web-console/app/adapters/api/gateway-profile-operations.ts @@ -0,0 +1,247 @@ +import type { + GatewayProfileControlPlane, + GatewayProfileCreateInput, + GatewayProfileFailureKind, + GatewayProfileInvocationContext, + GatewayProfileListRequest, + GatewayProfileRecord, +} from "@openshift-online/hypershell-gateway-management-ui"; +import { GatewayProfileOperationError } from "@openshift-online/hypershell-gateway-management-ui"; +import { + SDKAPIError, + type GatewayProfile, + type GatewayProfileCreateRequest, + type SDKClient, +} from "@openshift-online/hypershell-sdk"; + +type GatewayProfileApi = Pick< + SDKClient["gatewayProfiles"], + "create" | "delete" | "get" | "list" +>; +interface GatewayProfileApiClient { + gatewayProfiles: GatewayProfileApi; +} +type GatewayProfileApiFactory = ( + correlationId: string, +) => GatewayProfileApiClient; + +const gatewayProfileSortFields = { + created: "created_at", + name: "name", +} as const satisfies Record; + +function apiClient( + factory: GatewayProfileApiFactory, + context: GatewayProfileInvocationContext, +): GatewayProfileApiClient { + return factory(context.correlationId); +} + +function escapeIlikeLiteral(value: string): string { + // Escape backslashes first so the escapes added for SQL wildcards remain + // single escapes rather than being escaped again. + return value + .replaceAll("'", "''") + .replaceAll("\\", "\\\\") + .replaceAll("%", "\\%") + .replaceAll("_", "\\_"); +} + +function gatewayProfileSearch(value: string): string | undefined { + const query = value.trim(); + if (!query) { + return undefined; + } + return `name ilike '%${escapeIlikeLiteral(query)}%'`; +} + +function optionalString(value: string): string | undefined { + const trimmed = value.trim(); + return trimmed ? trimmed : undefined; +} + +function toGatewayProfileRecord(profile: GatewayProfile): GatewayProfileRecord { + const containerCpuLimitMax = optionalString(profile.container_cpu_limit_max); + const containerCpuRequestDefault = optionalString( + profile.container_cpu_request_default, + ); + const containerMemoryLimitMax = optionalString( + profile.container_memory_limit_max, + ); + const containerMemoryRequestDefault = optionalString( + profile.container_memory_request_default, + ); + const cpuLimitTotal = optionalString(profile.cpu_limit_total); + const cpuRequestTotal = optionalString(profile.cpu_request_total); + const description = optionalString(profile.description); + const ephemeralStorageTotal = optionalString(profile.ephemeral_storage_total); + const memoryLimitTotal = optionalString(profile.memory_limit_total); + const memoryRequestTotal = optionalString(profile.memory_request_total); + + return { + ...(containerCpuLimitMax ? { containerCpuLimitMax } : {}), + ...(containerCpuRequestDefault ? { containerCpuRequestDefault } : {}), + ...(containerMemoryLimitMax ? { containerMemoryLimitMax } : {}), + ...(containerMemoryRequestDefault ? { containerMemoryRequestDefault } : {}), + ...(cpuLimitTotal ? { cpuLimitTotal } : {}), + ...(cpuRequestTotal ? { cpuRequestTotal } : {}), + ...(profile.created_at ? { createdAt: profile.created_at } : {}), + ...(description ? { description } : {}), + ...(ephemeralStorageTotal ? { ephemeralStorageTotal } : {}), + id: profile.id, + ...(memoryLimitTotal ? { memoryLimitTotal } : {}), + ...(memoryRequestTotal ? { memoryRequestTotal } : {}), + name: profile.name, + podCount: profile.pod_count, + pvcCount: profile.pvc_count, + }; +} + +function toCreateRequest( + input: GatewayProfileCreateInput, +): GatewayProfileCreateRequest { + return { + ...(input.containerCpuLimitMax === undefined + ? {} + : { container_cpu_limit_max: input.containerCpuLimitMax }), + ...(input.containerCpuRequestDefault === undefined + ? {} + : { container_cpu_request_default: input.containerCpuRequestDefault }), + ...(input.containerMemoryLimitMax === undefined + ? {} + : { container_memory_limit_max: input.containerMemoryLimitMax }), + ...(input.containerMemoryRequestDefault === undefined + ? {} + : { + container_memory_request_default: input.containerMemoryRequestDefault, + }), + ...(input.cpuLimitTotal === undefined + ? {} + : { cpu_limit_total: input.cpuLimitTotal }), + ...(input.cpuRequestTotal === undefined + ? {} + : { cpu_request_total: input.cpuRequestTotal }), + ...(input.description === undefined + ? {} + : { description: input.description }), + ...(input.ephemeralStorageTotal === undefined + ? {} + : { ephemeral_storage_total: input.ephemeralStorageTotal }), + ...(input.memoryLimitTotal === undefined + ? {} + : { memory_limit_total: input.memoryLimitTotal }), + ...(input.memoryRequestTotal === undefined + ? {} + : { memory_request_total: input.memoryRequestTotal }), + name: input.name, + ...(input.podCount === undefined ? {} : { pod_count: input.podCount }), + ...(input.pvcCount === undefined ? {} : { pvc_count: input.pvcCount }), + }; +} + +function gatewayProfileFailureKind( + statusCode: number, +): GatewayProfileFailureKind { + if (statusCode === 401 || statusCode === 403) { + return "denied"; + } + if (statusCode === 404) { + return "not-found"; + } + if (statusCode === 409) { + return "conflict"; + } + if (statusCode === 408 || statusCode === 429 || statusCode >= 500) { + return "unavailable"; + } + return "unknown"; +} + +async function mapFailure(task: () => Promise): Promise { + try { + return await task(); + } catch (error) { + if (error instanceof SDKAPIError) { + throw new GatewayProfileOperationError( + gatewayProfileFailureKind(error.statusCode), + { + cause: error, + operationId: error.operationId || undefined, + }, + ); + } + throw error; + } +} + +export function createGatewayProfileControlPlaneAdapter( + apiFactory: GatewayProfileApiFactory, +): GatewayProfileControlPlane { + return { + async createGatewayProfile(input, context) { + return mapFailure(async () => + toGatewayProfileRecord( + await apiClient(apiFactory, context).gatewayProfiles.create( + toCreateRequest(input), + { signal: context.signal }, + ), + ), + ); + }, + async getGatewayProfile(gatewayProfileId, context) { + return mapFailure(async () => + toGatewayProfileRecord( + await apiClient(apiFactory, context).gatewayProfiles.get( + gatewayProfileId, + { signal: context.signal }, + ), + ), + ); + }, + async listGatewayProfiles(request, context) { + return mapFailure(async () => { + const search = gatewayProfileSearch(request.search); + const result = await apiClient( + apiFactory, + context, + ).gatewayProfiles.list( + { + orderBy: `${gatewayProfileSortFields[request.sortField]} ${request.sortDirection}`, + page: request.page, + ...(search === undefined ? {} : { search }), + size: request.size, + }, + { signal: context.signal }, + ); + const pageOffset = (request.page - 1) * request.size; + const expectedItemCount = Math.max( + 0, + Math.min(request.size, result.total - pageOffset), + ); + if ( + result.page !== request.page || + result.total < 0 || + result.items.length !== expectedItemCount + ) { + throw new GatewayProfileOperationError("unavailable"); + } + return { + items: result.items.map(toGatewayProfileRecord), + page: result.page, + size: request.size, + total: result.total, + }; + }); + }, + async removeGatewayProfile(gatewayProfileId, context) { + await mapFailure(() => + apiClient(apiFactory, context).gatewayProfiles.delete( + gatewayProfileId, + { + signal: context.signal, + }, + ), + ); + }, + }; +} diff --git a/components/web-console/app/adapters/observability/gateway-profile-observability.test.ts b/components/web-console/app/adapters/observability/gateway-profile-observability.test.ts new file mode 100644 index 00000000..45a10b82 --- /dev/null +++ b/components/web-console/app/adapters/observability/gateway-profile-observability.test.ts @@ -0,0 +1,98 @@ +import type { GatewayProfileProbe } from "@openshift-online/hypershell-gateway-management-ui"; +import { describe, expect, it, vi } from "vitest"; + +import { createGatewayProfileObservability } from "./gateway-profile-observability"; + +const testProbe: GatewayProfileProbe = { + context: { correlationId: "correlation-1" }, + fields: { action: "list", failureKind: null, outcome: "started" }, + name: "gatewayProfile.workflow.started", + occurredAt: "2026-08-06T18:00:00.000Z", + schemaVersion: 1, +}; + +describe("gateway profile observability adapter", () => { + it("fans out immutable probes and isolates a failing sink", () => { + const clearMarks = vi.fn(); + const mark = vi.fn< + (name: string, options?: PerformanceMarkOptions) => PerformanceMark + >(() => ({}) as PerformanceMark); + const observability = createGatewayProfileObservability({ + additionalSinks: [ + { + id: "failing-sink", + publish() { + throw new Error("unavailable"); + }, + }, + ], + performanceTarget: { clearMarks, mark }, + }); + + expect(() => { + observability.probes.publish(testProbe); + }).not.toThrow(); + + expect(observability.recentProbes()).toEqual([testProbe]); + expect(clearMarks).toHaveBeenCalledWith("gatewayProfile.workflow.started"); + expect(mark).toHaveBeenCalledWith("gatewayProfile.workflow.started", { + detail: testProbe, + }); + expect(observability.deliveryHealth()).toMatchObject({ + deliveryFailureCount: 1, + diagnosticFailureCount: 0, + lastFailure: { sinkId: "failing-sink" }, + }); + expect(observability.recentDeliveryFailures()).toEqual([ + expect.objectContaining({ sinkId: "failing-sink" }), + ]); + }); + + it("records an out-of-band delivery failure into delivery health", () => { + const observability = createGatewayProfileObservability({ + performanceTarget: { clearMarks: vi.fn(), mark: vi.fn() }, + }); + + observability.reportDeliveryFailure({ + errorType: "SpanExportError", + probeName: "gatewayProfile.trace.export", + schemaVersion: 0, + sinkId: "gateway-profile-trace", + }); + + expect(observability.deliveryHealth()).toMatchObject({ + deliveryFailureCount: 1, + lastFailure: { sinkId: "gateway-profile-trace" }, + }); + expect(observability.recentDeliveryFailures()).toEqual([ + expect.objectContaining({ probeName: "gatewayProfile.trace.export" }), + ]); + }); + + it("provides deterministic workflow context through injected capabilities", () => { + const observability = createGatewayProfileObservability({ + createCorrelationId: () => "correlation-1", + createTraceId: () => "0af7651916cd43dd8448eb211c80319c", + now: () => "2026-08-06T18:00:00.000Z", + performanceTarget: { clearMarks: vi.fn(), mark: vi.fn() }, + }); + + expect(observability.runtime.createCorrelationId()).toBe("correlation-1"); + expect(observability.runtime.createTraceId()).toBe( + "0af7651916cd43dd8448eb211c80319c", + ); + expect(observability.runtime.now()).toBe("2026-08-06T18:00:00.000Z"); + }); + + it("generates a valid W3C trace identifier by default", () => { + const observability = createGatewayProfileObservability({ + performanceTarget: { clearMarks: vi.fn(), mark: vi.fn() }, + }); + + const traceId = observability.runtime.createTraceId(); + + expect(traceId).toMatch(/^[0-9a-f]{32}$/); + expect(traceId).not.toBe("0".repeat(32)); + expect(observability.runtime.createTraceId()).not.toBe(traceId); + }); +}); diff --git a/components/web-console/app/adapters/observability/gateway-profile-observability.ts b/components/web-console/app/adapters/observability/gateway-profile-observability.ts new file mode 100644 index 00000000..a8f5c44e --- /dev/null +++ b/components/web-console/app/adapters/observability/gateway-profile-observability.ts @@ -0,0 +1,117 @@ +import type { + GatewayProfileProbe, + GatewayProfileProbePublisher, + GatewayWorkflowRuntime, +} from "@openshift-online/hypershell-gateway-management-ui"; +import { + FanOutDomainProbePublisher, + type DomainProbeSink, + type DomainProbeSinkSet, + type ProbeDeliveryFailure, + type ProbeDeliveryHealthSnapshot, +} from "@openshift-online/hypershell-domain-probes/fan-out"; + +const recentProbeLimit = 100; +const recentFailureLimit = 20; +const traceIdByteLength = 16; + +/** + * Creates a W3C trace identifier: a 16-byte random value rendered as 32 + * lowercase hex digits. A random 16-byte value is never the all-zero value + * that the W3C Trace Context specification forbids. + */ +function createTraceId(): string { + const bytes = new Uint8Array(traceIdByteLength); + globalThis.crypto.getRandomValues(bytes); + return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join( + "", + ); +} + +interface PerformanceProbeTarget { + clearMarks(name?: string): void; + mark(name: string, options?: PerformanceMarkOptions): PerformanceMark; +} + +export interface GatewayProfileObservability { + deliveryHealth(): Readonly; + probes: GatewayProfileProbePublisher; + recentDeliveryFailures(): readonly Readonly[]; + recentProbes(): readonly GatewayProfileProbe[]; + // Records a delivery failure that surfaces asynchronously, outside the + // synchronous probe fan-out, so a delivery the browser could not complete is + // counted in delivery health instead of being lost. + reportDeliveryFailure(failure: Readonly): void; + runtime: GatewayWorkflowRuntime; +} + +export interface GatewayProfileObservabilityOptions { + additionalSinks?: readonly DomainProbeSink[]; + createCorrelationId?: () => string; + createTraceId?: () => string; + now?: () => string; + performanceTarget?: PerformanceProbeTarget; +} + +function appendBounded(target: T[], value: T, limit: number) { + target.push(value); + if (target.length > limit) { + target.splice(0, target.length - limit); + } +} + +export function createGatewayProfileObservability( + options: GatewayProfileObservabilityOptions = {}, +): GatewayProfileObservability { + const failures: Readonly[] = []; + const recent: GatewayProfileProbe[] = []; + const performanceTarget = options.performanceTarget ?? globalThis.performance; + const recentSink: DomainProbeSink = { + id: "gateway-profile-product-health", + publish(value) { + appendBounded(recent, value, recentProbeLimit); + }, + }; + const performanceSink: DomainProbeSink = { + id: "gateway-profile-performance", + publish(value) { + performanceTarget.clearMarks(value.name); + performanceTarget.mark(value.name, { detail: value }); + }, + }; + const additionalSinks = options.additionalSinks ?? []; + const [firstAdditionalSink, ...remainingAdditionalSinks] = additionalSinks; + const sinks: DomainProbeSinkSet = + firstAdditionalSink === undefined + ? [recentSink, performanceSink] + : [ + recentSink, + firstAdditionalSink, + ...remainingAdditionalSinks, + performanceSink, + ]; + const publisher = new FanOutDomainProbePublisher({ + failureReporter: { + report(failure) { + appendBounded(failures, failure, recentFailureLimit); + }, + }, + sinks, + }); + + return { + deliveryHealth: () => publisher.healthSnapshot(), + probes: publisher, + recentDeliveryFailures: () => Object.freeze([...failures]), + recentProbes: () => Object.freeze([...recent]), + reportDeliveryFailure: (failure) => { + publisher.reportDeliveryFailure(failure); + }, + runtime: { + createCorrelationId: + options.createCorrelationId ?? (() => globalThis.crypto.randomUUID()), + createTraceId: options.createTraceId ?? createTraceId, + now: options.now ?? (() => new Date().toISOString()), + }, + }; +} diff --git a/components/web-console/app/composition/gateway-profile-composition.ts b/components/web-console/app/composition/gateway-profile-composition.ts new file mode 100644 index 00000000..224ab331 --- /dev/null +++ b/components/web-console/app/composition/gateway-profile-composition.ts @@ -0,0 +1,17 @@ +import { createGatewayProfileOperations } from "@openshift-online/hypershell-gateway-management-ui"; + +import { createApiClient } from "../adapters/api/api.client"; +import { createGatewayProfileControlPlaneAdapter } from "../adapters/api/gateway-profile-operations"; +import { createGatewayProfileObservability } from "../adapters/observability/gateway-profile-observability"; + +const gatewayProfileObservability = createGatewayProfileObservability(); + +const gatewayProfileControlPlane = createGatewayProfileControlPlaneAdapter( + (correlationId) => createApiClient(correlationId), +); + +export const gatewayProfileOperations = createGatewayProfileOperations({ + controlPlane: gatewayProfileControlPlane, + probes: gatewayProfileObservability.probes, + runtime: gatewayProfileObservability.runtime, +}); diff --git a/components/web-console/app/features/gateway-profiles/gateway-profile-list-state.test.ts b/components/web-console/app/features/gateway-profiles/gateway-profile-list-state.test.ts new file mode 100644 index 00000000..47d6e574 --- /dev/null +++ b/components/web-console/app/features/gateway-profiles/gateway-profile-list-state.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; + +import { + parseGatewayProfileListState, + serializeGatewayProfileListState, +} from "./gateway-profile-list-state"; + +describe("gateway profile list URL state", () => { + it("round-trips the authoritative collection controls", () => { + const state = parseGatewayProfileListState( + new URLSearchParams( + "q=small&page=3&size=50&sort=created&direction=desc&unrelated=ignored", + ), + ); + + expect(state).toEqual({ + page: 3, + search: "small", + size: 50, + sortDirection: "desc", + sortField: "created", + }); + expect(serializeGatewayProfileListState(state).toString()).toBe( + "q=small&page=3&size=50&sort=created&direction=desc", + ); + }); + + it("normalizes invalid controls to stable defaults", () => { + expect( + parseGatewayProfileListState( + new URLSearchParams("page=-1&size=999&sort=made-up&direction=sideways"), + ), + ).toEqual({ + page: 1, + search: "", + size: 20, + sortDirection: "asc", + sortField: "name", + }); + }); +}); diff --git a/components/web-console/app/features/gateway-profiles/gateway-profile-list-state.ts b/components/web-console/app/features/gateway-profiles/gateway-profile-list-state.ts new file mode 100644 index 00000000..8385a877 --- /dev/null +++ b/components/web-console/app/features/gateway-profiles/gateway-profile-list-state.ts @@ -0,0 +1,77 @@ +import { + defaultGatewayProfileListRequest, + gatewayProfileListPageSizes, + type GatewayProfileListRequest, +} from "@openshift-online/hypershell-gateway-management-ui"; + +function pageFrom(value: string | null): number { + if (!value || !/^[1-9][0-9]*$/u.test(value)) { + return defaultGatewayProfileListRequest.page; + } + const page = Number(value); + return Number.isSafeInteger(page) + ? page + : defaultGatewayProfileListRequest.page; +} + +function sortDirectionFrom(value: string | null) { + return value === "asc" || value === "desc" + ? value + : defaultGatewayProfileListRequest.sortDirection; +} + +function sizeFrom(value: string | null): number { + if (!value || !/^[1-9][0-9]*$/u.test(value)) { + return defaultGatewayProfileListRequest.size; + } + const size = Number(value); + return gatewayProfileListPageSizes.some((candidate) => candidate === size) + ? size + : defaultGatewayProfileListRequest.size; +} + +function sortFieldFrom(value: string | null) { + switch (value) { + case "created": + case "name": + return value; + default: + return defaultGatewayProfileListRequest.sortField; + } +} + +export function parseGatewayProfileListState( + parameters: URLSearchParams, +): GatewayProfileListRequest { + const sort = parameters.get("sort"); + const direction = parameters.get("direction"); + return { + page: pageFrom(parameters.get("page")), + search: parameters.get("q") ?? defaultGatewayProfileListRequest.search, + size: sizeFrom(parameters.get("size")), + sortDirection: sortDirectionFrom(direction), + sortField: sortFieldFrom(sort), + }; +} + +export function serializeGatewayProfileListState( + state: GatewayProfileListRequest, +): URLSearchParams { + const parameters = new URLSearchParams(); + if (state.search) { + parameters.set("q", state.search); + } + if (state.page !== defaultGatewayProfileListRequest.page) { + parameters.set("page", String(state.page)); + } + if (state.size !== defaultGatewayProfileListRequest.size) { + parameters.set("size", String(state.size)); + } + if (state.sortField !== defaultGatewayProfileListRequest.sortField) { + parameters.set("sort", state.sortField); + } + if (state.sortDirection !== defaultGatewayProfileListRequest.sortDirection) { + parameters.set("direction", state.sortDirection); + } + return parameters; +} diff --git a/components/web-console/app/features/shell/application-shell.test.tsx b/components/web-console/app/features/shell/application-shell.test.tsx index 7e6d0edb..240e01ca 100644 --- a/components/web-console/app/features/shell/application-shell.test.tsx +++ b/components/web-console/app/features/shell/application-shell.test.tsx @@ -1,7 +1,10 @@ import { render, screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { useGatewayUi } from "@openshift-online/hypershell-gateway-management-ui"; +import { + useGatewayProfileUi, + useGatewayUi, +} from "@openshift-online/hypershell-gateway-management-ui"; import { IntlProvider } from "react-intl"; import { MemoryRouter, Route, Routes, useLocation } from "react-router"; import { beforeEach, vi } from "vitest"; @@ -9,10 +12,12 @@ import { beforeEach, vi } from "vitest"; import { englishMessages } from "../../i18n/catalog"; import { ApplicationShell } from "./application-shell"; -const { getGatewayMock } = vi.hoisted(() => ({ +const { getGatewayMock, getGatewayProfileMock } = vi.hoisted(() => ({ getGatewayMock: vi.fn(), + getGatewayProfileMock: vi.fn(), })); const navigateToGatewayLabel = "Navigate to gateway"; +const navigateToGatewayProfileLabel = "Navigate to gateway profile"; vi.mock("../../composition/gateway-composition", () => ({ gatewayOperations: { @@ -27,6 +32,15 @@ vi.mock("../../composition/gateway-composition", () => ({ }, })); +vi.mock("../../composition/gateway-profile-composition", () => ({ + gatewayProfileOperations: { + createGatewayProfile: vi.fn(), + getGatewayProfile: getGatewayProfileMock, + listGatewayProfiles: vi.fn(), + removeGatewayProfile: vi.fn(), + }, +})); + // The masthead identity menu reads the session; keep it unauthenticated here so // shell assertions are unaffected. Menu behavior is covered in user-menu.test. vi.mock("../../composition/session-composition", () => ({ @@ -38,6 +52,7 @@ vi.mock("../../composition/session-composition", () => ({ function RouteContent() { const { pathname } = useLocation(); const { navigation } = useGatewayUi(); + const { navigation: profileNavigation } = useGatewayProfileUi(); return ( <> @@ -50,6 +65,16 @@ function RouteContent() { > {navigateToGatewayLabel} + ); } @@ -81,6 +106,10 @@ describe("ApplicationShell", () => { id: "gateway-b", name: "Friendly gateway", }); + getGatewayProfileMock.mockResolvedValue({ + id: "profile-b", + name: "Small profile", + }); }); it("provides the focused HyperShell gateway shell", () => { @@ -150,4 +179,40 @@ describe("ApplicationShell", () => { screen.getByRole("heading", { level: 1, name: "/" }), ); }); + + it("uses the profile name for a gateway profile deep link", async () => { + renderShell("/gateway-profiles/profile-b"); + + const breadcrumb = screen.getByRole("navigation", { name: "Breadcrumb" }); + expect( + within(breadcrumb) + .getByRole("link", { name: "Gateway profiles" }) + .getAttribute("href"), + ).toBe("/gateway-profiles"); + expect(await within(breadcrumb).findByText("Small profile")).toBeTruthy(); + }); + + it("identifies the gateway profile creation route", () => { + renderShell("/gateway-profiles/new"); + + const breadcrumb = screen.getByRole("navigation", { name: "Breadcrumb" }); + expect(within(breadcrumb).getByText("Gateway profiles")).toBeTruthy(); + expect(within(breadcrumb).getByText("Create gateway profile")).toBeTruthy(); + }); + + it("provides host navigation to the gateway profile package", async () => { + const user = userEvent.setup(); + renderShell(); + + await user.click( + screen.getByRole("button", { name: navigateToGatewayProfileLabel }), + ); + + expect( + screen.getByRole("heading", { + level: 1, + name: "/gateway-profiles/profile-b", + }), + ).toBeTruthy(); + }); }); diff --git a/components/web-console/app/features/shell/application-shell.tsx b/components/web-console/app/features/shell/application-shell.tsx index 242a80ba..41195b04 100644 --- a/components/web-console/app/features/shell/application-shell.tsx +++ b/components/web-console/app/features/shell/application-shell.tsx @@ -16,8 +16,12 @@ import { import { MoonIcon, SunIcon } from "@patternfly/react-icons"; import { gatewayMessages, + gatewayProfileMessages, + gatewayProfileQueryKey, + GatewayProfileUiProvider, gatewayQueryKey, GatewayUiProvider, + type GatewayProfileUiNavigation, type GatewayUiNavigation, } from "@openshift-online/hypershell-gateway-management-ui"; import { useQuery } from "@tanstack/react-query"; @@ -26,6 +30,7 @@ import { FormattedMessage, useIntl } from "react-intl"; import { Link, Outlet, useLocation, useNavigate } from "react-router"; import { gatewayOperations } from "../../composition/gateway-composition"; +import { gatewayProfileOperations } from "../../composition/gateway-profile-composition"; import { messages } from "../../i18n/messages"; import productLogo from "../../../../../images/brand/logo.png"; import { useColorScheme } from "./use-color-scheme"; @@ -46,6 +51,16 @@ export function ApplicationShell() { }), [navigate], ); + const gatewayProfileNavigation = useMemo( + () => ({ + collectionHref: "/gateway-profiles", + createHref: "/gateway-profiles/new", + detailHref: (gatewayProfileId) => + `/gateway-profiles/${encodeURIComponent(gatewayProfileId)}`, + navigate: (href, options) => navigate(href, options), + }), + [navigate], + ); const { scheme, toggle: toggleColorScheme } = useColorScheme(); useRouteHeadingFocus(pathname); const segments = pathname.split("/").filter(Boolean); @@ -58,6 +73,21 @@ export function ApplicationShell() { gatewayOperations.getGateway(gatewayId ?? "", signal), queryKey: gatewayQueryKey(gatewayId ?? ""), }); + const gatewayProfileSegment = + segments[0] === "gateway-profiles" ? segments[1] : undefined; + const isNewGatewayProfile = gatewayProfileSegment === "new"; + const gatewayProfileId = isNewGatewayProfile + ? undefined + : gatewayProfileSegment; + const gatewayProfileQuery = useQuery({ + enabled: gatewayProfileId !== undefined, + queryFn: ({ signal }) => + gatewayProfileOperations.getGatewayProfile( + gatewayProfileId ?? "", + signal, + ), + queryKey: gatewayProfileQueryKey(gatewayProfileId ?? ""), + }); const skipToContent = ( @@ -128,6 +158,27 @@ export function ApplicationShell() { ) : null} ); + } else if (gatewayProfileId || isNewGatewayProfile) { + breadcrumb = ( + + + + + {gatewayProfileId ? ( + + {gatewayProfileQuery.data?.name ?? + intl.formatMessage(gatewayProfileMessages.gatewayProfile)} + + ) : null} + {isNewGatewayProfile ? ( + + + + ) : null} + + ); } return ( @@ -135,15 +186,20 @@ export function ApplicationShell() { gateways={gatewayOperations} navigation={gatewayNavigation} > - - - + + + + ); } diff --git a/components/web-console/app/routes.ts b/components/web-console/app/routes.ts index c20aa10f..ab4afc03 100644 --- a/components/web-console/app/routes.ts +++ b/components/web-console/app/routes.ts @@ -13,6 +13,9 @@ export default [ index("./routes/home.tsx"), route(routeContract.gatewayNew, "./routes/gateway-new.tsx"), route(routeContract.gatewayDetail, "./routes/gateway.tsx"), + route(routeContract.gatewayProfiles, "./routes/gateway-profiles.tsx"), + route(routeContract.gatewayProfileNew, "./routes/gateway-profile-new.tsx"), + route(routeContract.gatewayProfileDetail, "./routes/gateway-profile.tsx"), ]), route("*", "./routes/not-found.tsx"), ] satisfies RouteConfig; diff --git a/components/web-console/app/routes/gateway-profile-new.tsx b/components/web-console/app/routes/gateway-profile-new.tsx new file mode 100644 index 00000000..596af9c7 --- /dev/null +++ b/components/web-console/app/routes/gateway-profile-new.tsx @@ -0,0 +1,9 @@ +import { GatewayProfileCreatePage } from "@openshift-online/hypershell-gateway-management-ui"; +import { createPageMeta } from "../lib/page-meta"; + +export const meta = createPageMeta( + "app.page.gatewayProfileCreate.title", + "app.page.gatewayProfileCreate.description", +); + +export default GatewayProfileCreatePage; diff --git a/components/web-console/app/routes/gateway-profile.tsx b/components/web-console/app/routes/gateway-profile.tsx new file mode 100644 index 00000000..93f15778 --- /dev/null +++ b/components/web-console/app/routes/gateway-profile.tsx @@ -0,0 +1,15 @@ +import { GatewayProfilePage } from "@openshift-online/hypershell-gateway-management-ui"; +import { useParams } from "react-router"; + +import { createPageMeta } from "../lib/page-meta"; + +export const meta = createPageMeta( + "app.page.gatewayProfileDetails.title", + "app.page.gatewayProfileDetails.description", +); + +export default function GatewayProfileRoute() { + const { gatewayProfileId = "" } = useParams(); + + return ; +} diff --git a/components/web-console/app/routes/gateway-profiles.tsx b/components/web-console/app/routes/gateway-profiles.tsx new file mode 100644 index 00000000..dc8687ec --- /dev/null +++ b/components/web-console/app/routes/gateway-profiles.tsx @@ -0,0 +1,44 @@ +import { GatewayProfilesPage } from "@openshift-online/hypershell-gateway-management-ui"; +import { useLocation, useNavigate, useSearchParams } from "react-router"; + +import { + parseGatewayProfileListState, + serializeGatewayProfileListState, +} from "../features/gateway-profiles/gateway-profile-list-state"; +import { createPageMeta } from "../lib/page-meta"; + +export const meta = createPageMeta( + "app.nav.gatewayProfiles", + "app.page.gatewayProfiles.description", +); + +export default function GatewayProfilesRoute() { + const location = useLocation(); + const navigate = useNavigate(); + const [searchParameters, setSearchParameters] = useSearchParams(); + const collectionState = parseGatewayProfileListState(searchParameters); + const deletedGatewayProfileName = + typeof (location.state as { deletedGatewayProfileName?: unknown } | null) + ?.deletedGatewayProfileName === "string" + ? (location.state as { deletedGatewayProfileName: string }) + .deletedGatewayProfileName + : undefined; + + return ( + { + void navigate(`${location.pathname}${location.search}`, { + replace: true, + state: null, + }); + }} + onCollectionStateChange={(state, reason) => { + setSearchParameters(serializeGatewayProfileListState(state), { + replace: reason === "filter", + }); + }} + /> + ); +} diff --git a/components/web-console/bff/src/app.ts b/components/web-console/bff/src/app.ts index 243113c3..7c4a0295 100644 --- a/components/web-console/bff/src/app.ts +++ b/components/web-console/bff/src/app.ts @@ -49,7 +49,10 @@ function isApplicationRoute(pathname: string): boolean { pathname === "/" || pathname === "/login" || pathname === "/gateways/new" || - /^\/gateways\/[^/]+\/?$/u.test(pathname) + /^\/gateways\/[^/]+\/?$/u.test(pathname) || + pathname === "/gateway-profiles" || + pathname === "/gateway-profiles/new" || + /^\/gateway-profiles\/[^/]+\/?$/u.test(pathname) ); } diff --git a/components/web-console/e2e/application-shell.spec.ts b/components/web-console/e2e/application-shell.spec.ts index ddfcda37..45a74581 100644 --- a/components/web-console/e2e/application-shell.spec.ts +++ b/components/web-console/e2e/application-shell.spec.ts @@ -26,7 +26,7 @@ const managedCluster = { id: "cluster-east", kind: "ManagedCluster", kubeconfig_secret: "cluster-east-kubeconfig", - name: "Cluster East", + name: "Hub cluster", provider: "AWS", region: "us-east-1", status: "Ready", @@ -137,7 +137,7 @@ test("makes gateway management the primary HyperShell experience", async ({ await expect( page .getByRole("grid", { name: "OpenShell Gateways" }) - .getByText("Cluster East"), + .getByText("Hub cluster"), ).toBeVisible(); const gatewayGrid = page.getByRole("grid", { name: "OpenShell Gateways" }); await expect( @@ -240,11 +240,11 @@ test("operates gateway rows and opens provisioning", async ({ page }) => { .getByRole("textbox", { name: "Filter by name, cluster, status, or endpoint", }) - .fill("Cluster East"); + .fill("Hub cluster"); await expect( page.getByRole("link", { name: "openshell-gateway-test", exact: true }), ).toBeVisible(); - await expect(page).toHaveURL(/\?q=Cluster\+East&sort=cluster$/u); + await expect(page).toHaveURL(/\?q=Hub\+cluster&sort=cluster$/u); await page.goBack(); await expect(page).toHaveURL(/\/$/u); await expect( @@ -253,9 +253,7 @@ test("operates gateway rows and opens provisioning", async ({ page }) => { await page.getByRole("link", { name: "Provision gateway" }).click(); await expect(page).toHaveURL(/\/gateways\/new$/); - await expect(page.getByRole("combobox", { name: "Cluster" })).toHaveValue( - "Hub cluster (default)", - ); + await expect(page.getByRole("combobox", { name: "Cluster" })).toHaveValue(""); await expect(page.getByLabel("Namespace", { exact: true })).toHaveCount(0); }); @@ -316,7 +314,7 @@ test("keeps connection methods on gateway details", async ({ // Operational configuration and copyable values live under the Details tab. await page.getByRole("tab", { name: "Details" }).click(); - await expect(page.getByText("Cluster East", { exact: true })).toBeVisible(); + await expect(page.getByText("Hub cluster", { exact: true })).toBeVisible(); await expect(page.getByText("cluster-east", { exact: true })).toHaveCount(0); await expect(page.getByText("Not available")).toHaveCount(0); await page @@ -494,11 +492,10 @@ test("provisions a gateway on an existing managed cluster", async ({ page.getByRole("heading", { level: 1, name: "Provision gateway" }), ).toBeFocused(); const clusterInput = page.getByRole("combobox", { name: "Cluster" }); - await expect(clusterInput).toHaveValue("Hub cluster (default)"); - await page.getByRole("button", { name: "Clear cluster search" }).click(); - await clusterInput.fill("East"); - await page.getByText("Cluster East", { exact: true }).click(); - await expect(clusterInput).toHaveValue("Cluster East"); + await expect(clusterInput).toHaveValue(""); + await clusterInput.fill("Hub"); + await page.getByText("Hub cluster", { exact: true }).click(); + await expect(clusterInput).toHaveValue("Hub cluster"); await expect(page.getByText("Provider: AWS; region: us-east-1")).toHaveCount( 0, ); diff --git a/components/web-console/locales/en.json b/components/web-console/locales/en.json index 377c6449..51021934 100644 --- a/components/web-console/locales/en.json +++ b/components/web-console/locales/en.json @@ -67,12 +67,8 @@ "defaultMessage": "Hub cluster", "description": "Placement label for the cluster hosting HyperShell." }, - "app.gateway.cluster.hubDefault": { - "defaultMessage": "Hub cluster (default)", - "description": "Default placement option for a gateway provisioning form." - }, "app.gateway.cluster.loadError.body": { - "defaultMessage": "You can provision on Hub cluster (default), or try loading managed clusters again.", + "defaultMessage": "Try loading managed clusters again.", "description": "Recovery guidance when remote gateway placements cannot be loaded." }, "app.gateway.cluster.loadError.title": { @@ -631,6 +627,138 @@ "defaultMessage": "Console unavailable for this gateway", "description": "Tooltip on the disabled console button once the console failed to become available within the expected time." }, + "app.gatewayProfile.clearSearch": { + "defaultMessage": "Clear gateway profile search", + "description": "Accessible label for clearing the gateway profile typeahead input." + }, + "app.gatewayProfile.containerCpuLimitMax": { + "defaultMessage": "Container CPU limit (max)", + "description": "Label for the maximum CPU limit allowed on a single container." + }, + "app.gatewayProfile.containerCpuRequestDefault": { + "defaultMessage": "Container CPU request (default)", + "description": "Label for the default CPU request applied to a container." + }, + "app.gatewayProfile.containerMemoryLimitMax": { + "defaultMessage": "Container memory limit (max)", + "description": "Label for the maximum memory limit allowed on a single container." + }, + "app.gatewayProfile.containerMemoryRequestDefault": { + "defaultMessage": "Container memory request (default)", + "description": "Label for the default memory request applied to a container." + }, + "app.gatewayProfile.cpuLimitTotal": { + "defaultMessage": "Total CPU limit", + "description": "Label for the total CPU limit across a gateway profile quota." + }, + "app.gatewayProfile.cpuRequestTotal": { + "defaultMessage": "Total CPU request", + "description": "Label for the total CPU request across a gateway profile quota." + }, + "app.gatewayProfile.delete": { + "defaultMessage": "Delete gateway profile", + "description": "Action that permanently deletes a gateway profile." + }, + "app.gatewayProfile.delete.confirmation": { + "defaultMessage": "Deleting {gatewayProfileName} will permanently remove the gateway profile. This action cannot be undone.", + "description": "Warning shown before permanently deleting a gateway profile." + }, + "app.gatewayProfile.delete.conflict.body": { + "defaultMessage": "Reassign or delete the gateways that use this profile before deleting it.", + "description": "Recovery guidance when a gateway profile is still referenced by gateways." + }, + "app.gatewayProfile.delete.conflict.title": { + "defaultMessage": "Gateway profile is in use", + "description": "Title shown when a gateway profile cannot be deleted because gateways reference it." + }, + "app.gatewayProfile.delete.error.body": { + "defaultMessage": "No changes were made. Try again.", + "description": "Recovery guidance when gateway profile deletion fails." + }, + "app.gatewayProfile.delete.error.title": { + "defaultMessage": "Gateway profile could not be deleted", + "description": "Title shown when gateway profile deletion fails." + }, + "app.gatewayProfile.delete.pending": { + "defaultMessage": "Deleting gateway profile", + "description": "Accessible progress text while a gateway profile is being deleted." + }, + "app.gatewayProfile.delete.title": { + "defaultMessage": "Delete {gatewayProfileName}?", + "description": "Title for the gateway profile deletion confirmation dialog." + }, + "app.gatewayProfile.deleted": { + "defaultMessage": "Gateway profile {gatewayProfileName} deleted", + "description": "Success notification after deleting a gateway profile." + }, + "app.gatewayProfile.description": { + "defaultMessage": "Description", + "description": "Label for a gateway profile description field and column." + }, + "app.gatewayProfile.ephemeralStorageTotal": { + "defaultMessage": "Total ephemeral storage", + "description": "Label for the total ephemeral storage across a gateway profile quota." + }, + "app.gatewayProfile.load.error.body": { + "defaultMessage": "Refresh the page to try again.", + "description": "Recovery guidance when gateway profile data cannot be loaded." + }, + "app.gatewayProfile.load.error.title": { + "defaultMessage": "Gateway profiles could not be loaded", + "description": "Title shown when gateway profile data cannot be loaded from the API." + }, + "app.gatewayProfile.loading": { + "defaultMessage": "Loading gateway profiles", + "description": "Accessible label shown while gateway profile data is loading." + }, + "app.gatewayProfile.memoryLimitTotal": { + "defaultMessage": "Total memory limit", + "description": "Label for the total memory limit across a gateway profile quota." + }, + "app.gatewayProfile.memoryRequestTotal": { + "defaultMessage": "Total memory request", + "description": "Label for the total memory request across a gateway profile quota." + }, + "app.gatewayProfile.moreResults": { + "defaultMessage": "More gateway profiles are available. Refine your search to find a specific profile.", + "description": "Guidance when a bounded gateway profile search has additional API results." + }, + "app.gatewayProfile.name": { + "defaultMessage": "Profile name", + "description": "Label for a gateway profile name field and column." + }, + "app.gatewayProfile.none": { + "defaultMessage": "No profile", + "description": "Option and value indicating that no gateway profile is selected." + }, + "app.gatewayProfile.podCount": { + "defaultMessage": "Pods", + "description": "Label for the maximum pod count in a gateway profile quota." + }, + "app.gatewayProfile.pvcCount": { + "defaultMessage": "Persistent volume claims", + "description": "Label for the maximum persistent volume claim count in a gateway profile quota." + }, + "app.gatewayProfile.quota.heading": { + "defaultMessage": "Resource quota", + "description": "Heading for the gateway profile resource quota section." + }, + "app.gatewayProfile.rowActions": { + "defaultMessage": "Actions for {gatewayProfileName}", + "description": "Accessible label for a gateway profile row actions menu." + }, + "app.gatewayProfile.select": { + "defaultMessage": "Select a gateway profile", + "description": "Accessible label and placeholder for the gateway profile selector." + }, + "app.gatewayProfile.singular": { + "defaultMessage": "Gateway profile", + "description": "Fallback breadcrumb label while a gateway profile is loading." + }, + "app.gatewayProfiles.refresh": { + "defaultMessage": "Refresh gateway profiles", + "description": "Accessible label for refreshing the gateway profile list." + }, "app.gateways.refresh": { "defaultMessage": "Refresh gateways", "description": "Accessible label for refreshing the gateway list." @@ -647,6 +775,10 @@ "defaultMessage": "Log out", "description": "Sign-out action in the masthead identity menu." }, + "app.nav.gatewayProfiles": { + "defaultMessage": "Gateway profiles", + "description": "Page and resource collection label for gateway resource quota profiles." + }, "app.nav.gateways": { "defaultMessage": "OpenShell Gateways", "description": "Page and resource collection label for OpenShell gateways." @@ -671,6 +803,58 @@ "defaultMessage": "Gateway details", "description": "Metadata title for a user-facing gateway details page." }, + "app.page.gatewayProfileCreate.description": { + "defaultMessage": "Configure a new gateway resource quota profile.", + "description": "Supporting text on the gateway profile creation page." + }, + "app.page.gatewayProfileCreate.error.body": { + "defaultMessage": "Check the values and try again.", + "description": "Recovery guidance when gateway profile creation fails." + }, + "app.page.gatewayProfileCreate.error.title": { + "defaultMessage": "Gateway profile could not be created", + "description": "Title shown when gateway profile creation fails." + }, + "app.page.gatewayProfileCreate.pending": { + "defaultMessage": "Creating gateway profile", + "description": "Accessible progress text while a gateway profile is being created." + }, + "app.page.gatewayProfileCreate.title": { + "defaultMessage": "Create gateway profile", + "description": "Page title and action for creating a gateway profile." + }, + "app.page.gatewayProfileDetails.description": { + "defaultMessage": "Review this gateway profile's resource quota.", + "description": "Metadata description for the gateway profile detail page." + }, + "app.page.gatewayProfileDetails.title": { + "defaultMessage": "Gateway profile details", + "description": "Metadata title for a gateway profile details page." + }, + "app.page.gatewayProfiles.description": { + "defaultMessage": "HyperShell gateway resource quota profiles.", + "description": "Browser metadata description for the gateway profiles page." + }, + "app.page.gatewayProfiles.empty.body": { + "defaultMessage": "Created gateway profiles will appear here.", + "description": "Body text when the gateway profile list is empty." + }, + "app.page.gatewayProfiles.empty.title": { + "defaultMessage": "No gateway profiles", + "description": "Heading when the gateway profile list is empty." + }, + "app.page.gatewayProfiles.filter": { + "defaultMessage": "Filter by name", + "description": "Placeholder and accessible label for gateway profile search." + }, + "app.page.gatewayProfiles.noResults.body": { + "defaultMessage": "Adjust or clear the filter to see gateway profiles.", + "description": "Guidance when no gateway profiles match the current filter." + }, + "app.page.gatewayProfiles.noResults.title": { + "defaultMessage": "No matching gateway profiles", + "description": "Heading when no gateway profiles match the current filter." + }, "app.page.gatewayProvision.description": { "defaultMessage": "Configure a new OpenShell gateway.", "description": "Supporting text on the gateway provisioning page." diff --git a/components/web-console/route-contract.json b/components/web-console/route-contract.json index 3ac15da3..fbc0c0a6 100644 --- a/components/web-console/route-contract.json +++ b/components/web-console/route-contract.json @@ -1,11 +1,17 @@ { "gatewayDetail": "gateways/:gatewayId", "gatewayNew": "gateways/new", + "gatewayProfileDetail": "gateway-profiles/:gatewayProfileId", + "gatewayProfileNew": "gateway-profiles/new", + "gatewayProfiles": "gateway-profiles", "login": "login", "directNavigationExamples": [ "/", "/login", "/gateways/new", - "/gateways/gateway-1" + "/gateways/gateway-1", + "/gateway-profiles", + "/gateway-profiles/new", + "/gateway-profiles/profile-1" ] } diff --git a/deploy/base/controller-rbac.yaml b/deploy/base/controller-rbac.yaml index d37e431b..67bf9aca 100644 --- a/deploy/base/controller-rbac.yaml +++ b/deploy/base/controller-rbac.yaml @@ -9,6 +9,11 @@ rules: - apiGroups: [""] resources: ["namespaces", "secrets", "configmaps", "services", "serviceaccounts", "persistentvolumeclaims"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] + # Per-namespace gateway quota enforcement (ResourceQuota + LimitRange) derived + # from the gateway's GatewayProfile. See openshell-gateway-quota.spec.md. + - apiGroups: [""] + resources: ["resourcequotas", "limitranges"] + verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] - apiGroups: ["apps"] resources: ["deployments", "statefulsets"] verbs: ["get", "list", "watch", "create", "update", "patch", "delete"] diff --git a/packages/gateway-management-ui/src/application/gateway-operations.test.ts b/packages/gateway-management-ui/src/application/gateway-operations.test.ts index 53c616df..304df570 100644 --- a/packages/gateway-management-ui/src/application/gateway-operations.test.ts +++ b/packages/gateway-management-ui/src/application/gateway-operations.test.ts @@ -42,6 +42,10 @@ function setup() { hasMore: false, items: [], }), + findGatewayProfiles: vi.fn().mockResolvedValue({ + hasMore: false, + items: [], + }), getGateway: vi.fn().mockResolvedValue(gateway), getGatewayPlacement: vi.fn().mockResolvedValue({ id: "cluster-east", @@ -111,6 +115,11 @@ describe("gateway application operations", () => { (operations: ReturnType["operations"]) => operations.findGatewayPlacements(" east "), ], + [ + "find-profiles", + (operations: ReturnType["operations"]) => + operations.findGatewayProfiles(" small "), + ], [ "get", (operations: ReturnType["operations"]) => diff --git a/packages/gateway-management-ui/src/application/gateway-operations.ts b/packages/gateway-management-ui/src/application/gateway-operations.ts index 08e5f090..8f16149d 100644 --- a/packages/gateway-management-ui/src/application/gateway-operations.ts +++ b/packages/gateway-management-ui/src/application/gateway-operations.ts @@ -200,6 +200,10 @@ export function createGatewayOperations({ execute("find-placements", signal, (context) => controlPlane.findGatewayPlacements(search.trim(), context), ), + findGatewayProfiles: (search, signal) => + execute("find-profiles", signal, (context) => + controlPlane.findGatewayProfiles(search.trim(), context), + ), getGatewayPlacement: (clusterId, signal) => execute("get-placement", signal, (context) => controlPlane.getGatewayPlacement(clusterId, context), diff --git a/packages/gateway-management-ui/src/application/gateway-probes.ts b/packages/gateway-management-ui/src/application/gateway-probes.ts index b80388b5..37b0aba5 100644 --- a/packages/gateway-management-ui/src/application/gateway-probes.ts +++ b/packages/gateway-management-ui/src/application/gateway-probes.ts @@ -9,6 +9,7 @@ export type GatewayAction = | "create-service-account" | "delete-service-account" | "find-placements" + | "find-profiles" | "get" | "get-placement" | "get-placements" diff --git a/packages/gateway-management-ui/src/application/gateway-profile-operations.test.ts b/packages/gateway-management-ui/src/application/gateway-profile-operations.test.ts new file mode 100644 index 00000000..2ea5d98a --- /dev/null +++ b/packages/gateway-management-ui/src/application/gateway-profile-operations.test.ts @@ -0,0 +1,224 @@ +import { describe, expect, it, vi } from "vitest"; + +import type { GatewayProfileProbe } from "./gateway-profile-probes"; +import { gatewayProfileProbeCatalog } from "./gateway-profile-probes"; +import { createGatewayProfileOperations } from "./gateway-profile-operations"; +import { + GatewayProfileOperationError, + type GatewayProfileControlPlane, + type GatewayProfileRecord, +} from "./gateway-profile-types"; + +const gatewayProfile: GatewayProfileRecord = { + cpuLimitTotal: "8", + id: "profile-1", + name: "Small profile", + podCount: 10, +}; +const listRequest = { + page: 1, + search: "", + size: 20, + sortDirection: "asc", + sortField: "name", +} as const; + +function setup() { + const received: GatewayProfileProbe[] = []; + let correlation = 0; + const listGatewayProfiles = vi.fn().mockResolvedValue({ + items: [gatewayProfile], + page: 1, + size: 20, + total: 1, + }); + const removeGatewayProfile = vi.fn().mockResolvedValue(undefined); + const controlPlane: GatewayProfileControlPlane = { + createGatewayProfile: vi.fn().mockResolvedValue(gatewayProfile), + getGatewayProfile: vi.fn().mockResolvedValue(gatewayProfile), + listGatewayProfiles, + removeGatewayProfile, + }; + const operations = createGatewayProfileOperations({ + controlPlane, + probes: { + publish(value) { + received.push(value); + }, + }, + runtime: { + createCorrelationId() { + correlation += 1; + return `correlation-${String(correlation)}`; + }, + createTraceId() { + return `trace-${String(correlation)}`; + }, + now() { + return "2026-08-06T18:00:00.000Z"; + }, + }, + }); + + return { + controlPlane, + listGatewayProfiles, + operations, + received, + removeGatewayProfile, + }; +} + +describe("gateway profile application operations", () => { + it.each([ + [ + "create", + (operations: ReturnType["operations"]) => + operations.createGatewayProfile({ name: "Small profile" }), + ], + [ + "get", + (operations: ReturnType["operations"]) => + operations.getGatewayProfile("profile-1"), + ], + [ + "list", + (operations: ReturnType["operations"]) => + operations.listGatewayProfiles(listRequest), + ], + [ + "remove", + (operations: ReturnType["operations"]) => + operations.removeGatewayProfile("profile-1"), + ], + ] as const)( + "publishes one successful %s workflow and dependency outcome", + async (action, invoke) => { + const { operations, received } = setup(); + + await invoke(operations); + + expect(received.map(({ name }) => name)).toEqual([ + "gatewayProfile.workflow.started", + "gatewayProfile.dependency.attempted", + "gatewayProfile.dependency.completed", + "gatewayProfile.workflow.completed", + ]); + expect(received.map(({ fields }) => fields)).toEqual([ + { action, failureKind: null, outcome: "started" }, + { action, failureKind: null, outcome: "started" }, + { action, failureKind: null, outcome: "succeeded" }, + { action, failureKind: null, outcome: "succeeded" }, + ]); + expect(received.every(Object.isFrozen)).toBe(true); + }, + ); + + it("passes one correlation identifier through the driven port", async () => { + const { listGatewayProfiles, operations, received } = setup(); + const abortController = new AbortController(); + + await operations.listGatewayProfiles(listRequest, abortController.signal); + + expect(listGatewayProfiles).toHaveBeenCalledWith(listRequest, { + correlationId: "correlation-1", + signal: abortController.signal, + }); + expect( + received.every( + ({ context }) => context.correlationId === "correlation-1", + ), + ).toBe(true); + expect(received.every(({ context }) => context.traceId === "trace-1")).toBe( + true, + ); + }); + + it("publishes a conflicted terminal outcome and preserves the typed failure", async () => { + const { operations, received, removeGatewayProfile } = setup(); + const failure = new GatewayProfileOperationError("conflict", { + operationId: "operation-1", + }); + removeGatewayProfile.mockRejectedValue(failure); + + await expect(operations.removeGatewayProfile("profile-1")).rejects.toBe( + failure, + ); + + expect(received.slice(-2).map(({ fields }) => fields)).toEqual([ + { action: "remove", failureKind: "conflict", outcome: "conflicted" }, + { action: "remove", failureKind: "conflict", outcome: "conflicted" }, + ]); + expect( + received.slice(-2).map(({ context }) => context.operationId), + ).toEqual(["operation-1", "operation-1"]); + }); + + it("maps a denied failure to a denied outcome", async () => { + const { operations, received, removeGatewayProfile } = setup(); + removeGatewayProfile.mockRejectedValue( + new GatewayProfileOperationError("denied"), + ); + + await expect(operations.removeGatewayProfile("profile-1")).rejects.toThrow( + GatewayProfileOperationError, + ); + + expect(received.slice(-1).map(({ fields }) => fields)).toEqual([ + { action: "remove", failureKind: "denied", outcome: "denied" }, + ]); + }); + + it("maps an unknown failure to a failed outcome", async () => { + const { operations, received, removeGatewayProfile } = setup(); + removeGatewayProfile.mockRejectedValue(new Error("boom")); + + await expect(operations.removeGatewayProfile("profile-1")).rejects.toThrow( + "boom", + ); + + expect(received.slice(-1).map(({ fields }) => fields)).toEqual([ + { action: "remove", failureKind: "unknown", outcome: "failed" }, + ]); + }); + + it("publishes cancellation without turning it into an application error", async () => { + const { listGatewayProfiles, operations, received } = setup(); + const cancellation = Object.assign(new Error("cancelled"), { + name: "AbortError", + }); + listGatewayProfiles.mockRejectedValue(cancellation); + + await expect(operations.listGatewayProfiles(listRequest)).rejects.toBe( + cancellation, + ); + + expect(received.slice(-2).map(({ fields }) => fields)).toEqual([ + { action: "list", failureKind: "cancelled", outcome: "cancelled" }, + { action: "list", failureKind: "cancelled", outcome: "cancelled" }, + ]); + }); + + it("sets a descriptive message on the operation error", () => { + expect(new GatewayProfileOperationError("not-found").message).toBe( + "Gateway profile operation failed: not-found", + ); + }); + + it("keeps the probe catalog synchronized with the closed schema names", () => { + expect(gatewayProfileProbeCatalog.map(({ name }) => name)).toEqual([ + "gatewayProfile.workflow.started", + "gatewayProfile.workflow.completed", + "gatewayProfile.dependency.attempted", + "gatewayProfile.dependency.completed", + ]); + }); + + it("declares trace as an allowed consumer for every gateway profile probe", () => { + expect( + gatewayProfileProbeCatalog.every(({ allowedConsumers }) => + allowedConsumers.includes("trace"), + ), + ).toBe(true); + }); +}); diff --git a/packages/gateway-management-ui/src/application/gateway-profile-operations.ts b/packages/gateway-management-ui/src/application/gateway-profile-operations.ts new file mode 100644 index 00000000..8c83e2a3 --- /dev/null +++ b/packages/gateway-management-ui/src/application/gateway-profile-operations.ts @@ -0,0 +1,198 @@ +import type { + GatewayProfileAction, + GatewayProfileProbe, + GatewayProfileProbeOutcome, + GatewayProfileProbePublisher, +} from "./gateway-profile-probes"; +import { + GatewayProfileOperationError, + type GatewayProfileControlPlane, + type GatewayProfileInvocationContext, + type GatewayProfileOperations, + type GatewayWorkflowRuntime, +} from "./gateway-profile-types"; + +export interface GatewayProfileOperationDependencies { + controlPlane: GatewayProfileControlPlane; + probes: GatewayProfileProbePublisher; + runtime: GatewayWorkflowRuntime; +} + +function isCancelled(error: unknown): boolean { + return ( + typeof error === "object" && + error !== null && + "name" in error && + error.name === "AbortError" + ); +} + +function failureKind(error: unknown) { + if (isCancelled(error)) { + return "cancelled" as const; + } + return error instanceof GatewayProfileOperationError ? error.kind : "unknown"; +} + +function failureOutcome(error: unknown): GatewayProfileProbeOutcome { + const kind = failureKind(error); + if (kind === "cancelled") { + return "cancelled"; + } + if (kind === "conflict") { + return "conflicted"; + } + if (kind === "denied") { + return "denied"; + } + return "failed"; +} + +function probe( + action: GatewayProfileAction, + correlationId: string, + traceId: string, + failure: ReturnType | null, + name: GatewayProfileProbe["name"], + occurredAt: string, + outcome: GatewayProfileProbeOutcome, + operationId?: string, + parentInvocationId?: string, +): GatewayProfileProbe { + return Object.freeze({ + context: Object.freeze({ + correlationId, + traceId, + ...(operationId === undefined ? {} : { operationId }), + ...(parentInvocationId === undefined ? {} : { parentInvocationId }), + }), + fields: Object.freeze({ action, failureKind: failure, outcome }), + name, + occurredAt, + schemaVersion: 1, + }); +} + +export function createGatewayProfileOperations({ + controlPlane, + probes, + runtime, +}: GatewayProfileOperationDependencies): GatewayProfileOperations { + async function execute( + action: GatewayProfileAction, + signal: AbortSignal | undefined, + task: (context: GatewayProfileInvocationContext) => Promise, + ): Promise { + const correlationId = runtime.createCorrelationId(); + const traceId = runtime.createTraceId(); + const context: GatewayProfileInvocationContext = { + correlationId, + ...(signal === undefined ? {} : { signal }), + }; + probes.publish( + probe( + action, + correlationId, + traceId, + null, + "gatewayProfile.workflow.started", + runtime.now(), + "started", + ), + ); + probes.publish( + probe( + action, + correlationId, + traceId, + null, + "gatewayProfile.dependency.attempted", + runtime.now(), + "started", + undefined, + correlationId, + ), + ); + + try { + const result = await task(context); + probes.publish( + probe( + action, + correlationId, + traceId, + null, + "gatewayProfile.dependency.completed", + runtime.now(), + "succeeded", + undefined, + correlationId, + ), + ); + probes.publish( + probe( + action, + correlationId, + traceId, + null, + "gatewayProfile.workflow.completed", + runtime.now(), + "succeeded", + ), + ); + return result; + } catch (error) { + const kind = failureKind(error); + const outcome = failureOutcome(error); + const operationId = + error instanceof GatewayProfileOperationError + ? error.operationId + : undefined; + probes.publish( + probe( + action, + correlationId, + traceId, + kind, + "gatewayProfile.dependency.completed", + runtime.now(), + outcome, + operationId, + correlationId, + ), + ); + probes.publish( + probe( + action, + correlationId, + traceId, + kind, + "gatewayProfile.workflow.completed", + runtime.now(), + outcome, + operationId, + ), + ); + throw error; + } + } + + return { + createGatewayProfile: (input, signal) => + execute("create", signal, (context) => + controlPlane.createGatewayProfile(input, context), + ), + getGatewayProfile: (gatewayProfileId, signal) => + execute("get", signal, (context) => + controlPlane.getGatewayProfile(gatewayProfileId, context), + ), + listGatewayProfiles: (request, signal) => + execute("list", signal, (context) => + controlPlane.listGatewayProfiles(request, context), + ), + removeGatewayProfile: (gatewayProfileId, signal) => + execute("remove", signal, (context) => + controlPlane.removeGatewayProfile(gatewayProfileId, context), + ), + }; +} diff --git a/packages/gateway-management-ui/src/application/gateway-profile-probes.ts b/packages/gateway-management-ui/src/application/gateway-profile-probes.ts new file mode 100644 index 00000000..d5f3b891 --- /dev/null +++ b/packages/gateway-management-ui/src/application/gateway-profile-probes.ts @@ -0,0 +1,104 @@ +import type { + DomainProbe, + DomainProbePublisher, +} from "@openshift-online/hypershell-domain-probes"; + +import type { GatewayProfileFailureKind } from "./gateway-profile-types"; + +export type GatewayProfileAction = "create" | "get" | "list" | "remove"; +export type GatewayProfileProbeOutcome = + "started" | "succeeded" | "failed" | "cancelled" | "denied" | "conflicted"; +export type GatewayProfileProbeName = + | "gatewayProfile.workflow.started" + | "gatewayProfile.workflow.completed" + | "gatewayProfile.dependency.attempted" + | "gatewayProfile.dependency.completed"; + +export type GatewayProfileProbe = DomainProbe< + GatewayProfileProbeName, + 1, + { + readonly action: GatewayProfileAction; + readonly failureKind: GatewayProfileFailureKind | null; + readonly outcome: GatewayProfileProbeOutcome; + } +>; + +export type GatewayProfileProbePublisher = + DomainProbePublisher; + +export const gatewayProfileProbeCatalog = Object.freeze([ + { + allowedConsumers: [ + "structured-log", + "performance", + "product-health", + "trace", + ], + deliveryClass: "best-effort", + fields: { + action: "bounded operational enum", + failureKind: "bounded operational enum; no raw error content", + outcome: "bounded operational enum", + }, + name: "gatewayProfile.workflow.started", + owner: "gatewayProfile", + schemaVersion: 1, + trigger: "A gateway-profile application use case starts", + }, + { + allowedConsumers: [ + "structured-log", + "performance", + "product-health", + "trace", + ], + deliveryClass: "best-effort", + fields: { + action: "bounded operational enum", + failureKind: "bounded operational enum; no raw error content", + outcome: "bounded operational enum", + }, + name: "gatewayProfile.workflow.completed", + owner: "gatewayProfile", + schemaVersion: 1, + trigger: + "A gateway-profile application use case reaches one terminal outcome", + }, + { + allowedConsumers: [ + "structured-log", + "performance", + "product-health", + "trace", + ], + deliveryClass: "best-effort", + fields: { + action: "bounded operational enum", + failureKind: "bounded operational enum; no raw error content", + outcome: "bounded operational enum", + }, + name: "gatewayProfile.dependency.attempted", + owner: "gatewayProfile", + schemaVersion: 1, + trigger: "A gateway-profile control-plane dependency attempt starts", + }, + { + allowedConsumers: [ + "structured-log", + "performance", + "product-health", + "trace", + ], + deliveryClass: "best-effort", + fields: { + action: "bounded operational enum", + failureKind: "bounded operational enum; no raw error content", + outcome: "bounded operational enum", + }, + name: "gatewayProfile.dependency.completed", + owner: "gatewayProfile", + schemaVersion: 1, + trigger: "A gateway-profile control-plane dependency attempt completes", + }, +] as const); diff --git a/packages/gateway-management-ui/src/application/gateway-profile-types.ts b/packages/gateway-management-ui/src/application/gateway-profile-types.ts new file mode 100644 index 00000000..c5e9fda3 --- /dev/null +++ b/packages/gateway-management-ui/src/application/gateway-profile-types.ts @@ -0,0 +1,131 @@ +import type { GatewayWorkflowRuntime } from "./gateway-types"; + +export type { GatewayWorkflowRuntime }; + +export interface GatewayProfileRecord { + containerCpuLimitMax?: string; + containerCpuRequestDefault?: string; + containerMemoryLimitMax?: string; + containerMemoryRequestDefault?: string; + cpuLimitTotal?: string; + cpuRequestTotal?: string; + createdAt?: string; + description?: string; + ephemeralStorageTotal?: string; + id: string; + memoryLimitTotal?: string; + memoryRequestTotal?: string; + name: string; + podCount?: number; + pvcCount?: number; +} + +export type GatewayProfileSortDirection = "asc" | "desc"; +export type GatewayProfileSortField = "created" | "name"; + +export interface GatewayProfileListRequest { + page: number; + search: string; + size: number; + sortDirection: GatewayProfileSortDirection; + sortField: GatewayProfileSortField; +} + +export const gatewayProfileListPageSizes = [10, 20, 50, 100] as const; + +export const defaultGatewayProfileListRequest: Readonly = + Object.freeze({ + page: 1, + search: "", + size: gatewayProfileListPageSizes[1], + sortDirection: "asc", + sortField: "name", + }); + +export interface GatewayProfilePage { + items: readonly T[]; + page: number; + size: number; + total: number; +} + +export interface GatewayProfileInvocationContext { + correlationId: string; + signal?: AbortSignal; +} + +export interface GatewayProfileCreateInput { + containerCpuLimitMax?: string; + containerCpuRequestDefault?: string; + containerMemoryLimitMax?: string; + containerMemoryRequestDefault?: string; + cpuLimitTotal?: string; + cpuRequestTotal?: string; + description?: string; + ephemeralStorageTotal?: string; + memoryLimitTotal?: string; + memoryRequestTotal?: string; + name: string; + podCount?: number; + pvcCount?: number; +} + +export type GatewayProfileFailureKind = + "cancelled" | "conflict" | "denied" | "not-found" | "unavailable" | "unknown"; + +export class GatewayProfileOperationError extends Error { + readonly kind: GatewayProfileFailureKind; + readonly operationId?: string; + + constructor( + kind: GatewayProfileFailureKind, + options: ErrorOptions & { + operationId?: string; + } = {}, + ) { + super(`Gateway profile operation failed: ${kind}`, options); + this.name = "GatewayProfileOperationError"; + this.kind = kind; + this.operationId = options.operationId; + } +} + +/** Application-owned driven port for HyperShell gateway profiles. */ +export interface GatewayProfileControlPlane { + createGatewayProfile( + input: GatewayProfileCreateInput, + context: GatewayProfileInvocationContext, + ): Promise; + getGatewayProfile( + gatewayProfileId: string, + context: GatewayProfileInvocationContext, + ): Promise; + listGatewayProfiles( + request: GatewayProfileListRequest, + context: GatewayProfileInvocationContext, + ): Promise>; + removeGatewayProfile( + gatewayProfileId: string, + context: GatewayProfileInvocationContext, + ): Promise; +} + +/** Driving entry port used by the GatewayProfile UI presentation adapters. */ +export interface GatewayProfileOperations { + createGatewayProfile( + input: GatewayProfileCreateInput, + signal?: AbortSignal, + ): Promise; + getGatewayProfile( + gatewayProfileId: string, + signal?: AbortSignal, + ): Promise; + listGatewayProfiles( + request: GatewayProfileListRequest, + signal?: AbortSignal, + ): Promise>; + removeGatewayProfile( + gatewayProfileId: string, + signal?: AbortSignal, + ): Promise; +} diff --git a/packages/gateway-management-ui/src/application/gateway-types.ts b/packages/gateway-management-ui/src/application/gateway-types.ts index 98d6c46f..8c154602 100644 --- a/packages/gateway-management-ui/src/application/gateway-types.ts +++ b/packages/gateway-management-ui/src/application/gateway-types.ts @@ -13,13 +13,26 @@ export interface GatewayRecord { oidcClientId?: string; oidcIssuer?: string; phase?: string; + profileId?: string; releaseId: string; status?: string; } +export interface GatewayProfileSummary { + description?: string; + id: string; + name: string; +} + +export interface GatewayProfileSummaryOptions { + hasMore: boolean; + items: readonly GatewayProfileSummary[]; +} + export interface GatewayPlacement { id: string; name: string; + profileId?: string; provider: string; region?: string; status?: string; @@ -68,6 +81,7 @@ export interface GatewayInvocationContext { export interface GatewayProvisionInput { clusterId: string; name: string; + profileId?: string; } export type OpenShellGatewayServiceAccountRole = @@ -218,6 +232,10 @@ export interface GatewayControlPlane { search: string, context: GatewayInvocationContext, ): Promise; + findGatewayProfiles( + search: string, + context: GatewayInvocationContext, + ): Promise; getGatewayPlacement( clusterId: string, context: GatewayInvocationContext, @@ -280,6 +298,10 @@ export interface GatewayOperations { search: string, signal?: AbortSignal, ): Promise; + findGatewayProfiles( + search: string, + signal?: AbortSignal, + ): Promise; getGatewayPlacement( clusterId: string, signal?: AbortSignal, diff --git a/packages/gateway-management-ui/src/gateway-profile-messages.ts b/packages/gateway-management-ui/src/gateway-profile-messages.ts new file mode 100644 index 00000000..c87bd72f --- /dev/null +++ b/packages/gateway-management-ui/src/gateway-profile-messages.ts @@ -0,0 +1,263 @@ +import { defineMessages } from "react-intl"; + +export const gatewayProfileMessages = defineMessages({ + clearGatewayProfileSearch: { + id: "app.gatewayProfile.clearSearch", + defaultMessage: "Clear gateway profile search", + description: + "Accessible label for clearing the gateway profile typeahead input.", + }, + containerCpuLimitMax: { + id: "app.gatewayProfile.containerCpuLimitMax", + defaultMessage: "Container CPU limit (max)", + description: + "Label for the maximum CPU limit allowed on a single container.", + }, + containerCpuRequestDefault: { + id: "app.gatewayProfile.containerCpuRequestDefault", + defaultMessage: "Container CPU request (default)", + description: "Label for the default CPU request applied to a container.", + }, + containerMemoryLimitMax: { + id: "app.gatewayProfile.containerMemoryLimitMax", + defaultMessage: "Container memory limit (max)", + description: + "Label for the maximum memory limit allowed on a single container.", + }, + containerMemoryRequestDefault: { + id: "app.gatewayProfile.containerMemoryRequestDefault", + defaultMessage: "Container memory request (default)", + description: "Label for the default memory request applied to a container.", + }, + cpuLimitTotal: { + id: "app.gatewayProfile.cpuLimitTotal", + defaultMessage: "Total CPU limit", + description: + "Label for the total CPU limit across a gateway profile quota.", + }, + cpuRequestTotal: { + id: "app.gatewayProfile.cpuRequestTotal", + defaultMessage: "Total CPU request", + description: + "Label for the total CPU request across a gateway profile quota.", + }, + createGatewayProfile: { + id: "app.page.gatewayProfileCreate.title", + defaultMessage: "Create gateway profile", + description: "Page title and action for creating a gateway profile.", + }, + createGatewayProfileDescription: { + id: "app.page.gatewayProfileCreate.description", + defaultMessage: "Configure a new gateway resource quota profile.", + description: "Supporting text on the gateway profile creation page.", + }, + creatingGatewayProfile: { + id: "app.page.gatewayProfileCreate.pending", + defaultMessage: "Creating gateway profile", + description: + "Accessible progress text while a gateway profile is being created.", + }, + deleteGatewayProfile: { + id: "app.gatewayProfile.delete", + defaultMessage: "Delete gateway profile", + description: "Action that permanently deletes a gateway profile.", + }, + deleteGatewayProfileConfirmation: { + id: "app.gatewayProfile.delete.confirmation", + defaultMessage: + "Deleting {gatewayProfileName} will permanently remove the gateway profile. This action cannot be undone.", + description: "Warning shown before permanently deleting a gateway profile.", + }, + deleteGatewayProfileTitle: { + id: "app.gatewayProfile.delete.title", + defaultMessage: "Delete {gatewayProfileName}?", + description: "Title for the gateway profile deletion confirmation dialog.", + }, + deletingGatewayProfile: { + id: "app.gatewayProfile.delete.pending", + defaultMessage: "Deleting gateway profile", + description: + "Accessible progress text while a gateway profile is being deleted.", + }, + ephemeralStorageTotal: { + id: "app.gatewayProfile.ephemeralStorageTotal", + defaultMessage: "Total ephemeral storage", + description: + "Label for the total ephemeral storage across a gateway profile quota.", + }, + filterGatewayProfiles: { + id: "app.page.gatewayProfiles.filter", + defaultMessage: "Filter by name", + description: "Placeholder and accessible label for gateway profile search.", + }, + gatewayProfile: { + id: "app.gatewayProfile.singular", + defaultMessage: "Gateway profile", + description: + "Fallback breadcrumb label while a gateway profile is loading.", + }, + gatewayProfileDeleteConflict: { + id: "app.gatewayProfile.delete.conflict.title", + defaultMessage: "Gateway profile is in use", + description: + "Title shown when a gateway profile cannot be deleted because gateways reference it.", + }, + gatewayProfileDeleteConflictBody: { + id: "app.gatewayProfile.delete.conflict.body", + defaultMessage: + "Reassign or delete the gateways that use this profile before deleting it.", + description: + "Recovery guidance when a gateway profile is still referenced by gateways.", + }, + gatewayProfileDeleted: { + id: "app.gatewayProfile.deleted", + defaultMessage: "Gateway profile {gatewayProfileName} deleted", + description: "Success notification after deleting a gateway profile.", + }, + gatewayProfileDeleteError: { + id: "app.gatewayProfile.delete.error.title", + defaultMessage: "Gateway profile could not be deleted", + description: "Title shown when gateway profile deletion fails.", + }, + gatewayProfileDeleteErrorBody: { + id: "app.gatewayProfile.delete.error.body", + defaultMessage: "No changes were made. Try again.", + description: "Recovery guidance when gateway profile deletion fails.", + }, + gatewayProfileDescription: { + id: "app.gatewayProfile.description", + defaultMessage: "Description", + description: "Label for a gateway profile description field and column.", + }, + gatewayProfileDetailDescription: { + id: "app.page.gatewayProfileDetails.description", + defaultMessage: "Review this gateway profile's resource quota.", + description: "Metadata description for the gateway profile detail page.", + }, + gatewayProfileDetailsTitle: { + id: "app.page.gatewayProfileDetails.title", + defaultMessage: "Gateway profile details", + description: "Metadata title for a gateway profile details page.", + }, + gatewayProfileLoadError: { + id: "app.gatewayProfile.load.error.title", + defaultMessage: "Gateway profiles could not be loaded", + description: + "Title shown when gateway profile data cannot be loaded from the API.", + }, + gatewayProfileLoadErrorBody: { + id: "app.gatewayProfile.load.error.body", + defaultMessage: "Refresh the page to try again.", + description: + "Recovery guidance when gateway profile data cannot be loaded.", + }, + gatewayProfileName: { + id: "app.gatewayProfile.name", + defaultMessage: "Profile name", + description: "Label for a gateway profile name field and column.", + }, + gatewayProfileNone: { + id: "app.gatewayProfile.none", + defaultMessage: "No profile", + description: + "Option and value indicating that no gateway profile is selected.", + }, + gatewayProfiles: { + id: "app.nav.gatewayProfiles", + defaultMessage: "Gateway profiles", + description: + "Page and resource collection label for gateway resource quota profiles.", + }, + gatewayProfilesDescription: { + id: "app.page.gatewayProfiles.description", + defaultMessage: "HyperShell gateway resource quota profiles.", + description: "Browser metadata description for the gateway profiles page.", + }, + gatewayProfilesEmptyBody: { + id: "app.page.gatewayProfiles.empty.body", + defaultMessage: "Created gateway profiles will appear here.", + description: "Body text when the gateway profile list is empty.", + }, + gatewayProfilesEmptyTitle: { + id: "app.page.gatewayProfiles.empty.title", + defaultMessage: "No gateway profiles", + description: "Heading when the gateway profile list is empty.", + }, + gatewayProfilesProvisionError: { + id: "app.page.gatewayProfileCreate.error.title", + defaultMessage: "Gateway profile could not be created", + description: "Title shown when gateway profile creation fails.", + }, + gatewayProfilesProvisionErrorBody: { + id: "app.page.gatewayProfileCreate.error.body", + defaultMessage: "Check the values and try again.", + description: "Recovery guidance when gateway profile creation fails.", + }, + gatewayProfileRowActions: { + id: "app.gatewayProfile.rowActions", + defaultMessage: "Actions for {gatewayProfileName}", + description: "Accessible label for a gateway profile row actions menu.", + }, + loadingGatewayProfiles: { + id: "app.gatewayProfile.loading", + defaultMessage: "Loading gateway profiles", + description: + "Accessible label shown while gateway profile data is loading.", + }, + memoryLimitTotal: { + id: "app.gatewayProfile.memoryLimitTotal", + defaultMessage: "Total memory limit", + description: + "Label for the total memory limit across a gateway profile quota.", + }, + memoryRequestTotal: { + id: "app.gatewayProfile.memoryRequestTotal", + defaultMessage: "Total memory request", + description: + "Label for the total memory request across a gateway profile quota.", + }, + moreGatewayProfilesAvailable: { + id: "app.gatewayProfile.moreResults", + defaultMessage: + "More gateway profiles are available. Refine your search to find a specific profile.", + description: + "Guidance when a bounded gateway profile search has additional API results.", + }, + noMatchingGatewayProfiles: { + id: "app.page.gatewayProfiles.noResults.title", + defaultMessage: "No matching gateway profiles", + description: "Heading when no gateway profiles match the current filter.", + }, + noMatchingGatewayProfilesBody: { + id: "app.page.gatewayProfiles.noResults.body", + defaultMessage: "Adjust or clear the filter to see gateway profiles.", + description: "Guidance when no gateway profiles match the current filter.", + }, + podCount: { + id: "app.gatewayProfile.podCount", + defaultMessage: "Pods", + description: "Label for the maximum pod count in a gateway profile quota.", + }, + pvcCount: { + id: "app.gatewayProfile.pvcCount", + defaultMessage: "Persistent volume claims", + description: + "Label for the maximum persistent volume claim count in a gateway profile quota.", + }, + quotaHeading: { + id: "app.gatewayProfile.quota.heading", + defaultMessage: "Resource quota", + description: "Heading for the gateway profile resource quota section.", + }, + refreshGatewayProfiles: { + id: "app.gatewayProfiles.refresh", + defaultMessage: "Refresh gateway profiles", + description: "Accessible label for refreshing the gateway profile list.", + }, + selectGatewayProfile: { + id: "app.gatewayProfile.select", + defaultMessage: "Select a gateway profile", + description: + "Accessible label and placeholder for the gateway profile selector.", + }, +}); diff --git a/packages/gateway-management-ui/src/gateway-profile-ui-provider.test.tsx b/packages/gateway-management-ui/src/gateway-profile-ui-provider.test.tsx new file mode 100644 index 00000000..98107848 --- /dev/null +++ b/packages/gateway-management-ui/src/gateway-profile-ui-provider.test.tsx @@ -0,0 +1,95 @@ +import { render, screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { vi } from "vitest"; + +import { + GatewayProfileUiProvider, + useGatewayProfileLink, + useGatewayProfileUi, +} from "./gateway-profile-ui-provider"; + +const gatewayProfileOperations = { + createGatewayProfile: vi.fn(), + getGatewayProfile: vi.fn(), + listGatewayProfiles: vi.fn(), + removeGatewayProfile: vi.fn(), +}; + +const navigate = vi.fn(); +const navigation = { + collectionHref: "/gateway-profiles", + createHref: "/gateway-profiles/new", + detailHref: (gatewayProfileId: string) => + `/gateway-profiles/${gatewayProfileId}`, + navigate, +}; + +const gatewayProfileLabel = "Gateway profile"; + +function TestLink() { + const link = useGatewayProfileLink("#profile-1"); + + return {gatewayProfileLabel}; +} + +function ServicesConsumer() { + const services = useGatewayProfileUi(); + + return {services.navigation.createHref}; +} + +describe("GatewayProfileUiProvider", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("exposes host services and intercepts an unmodified link activation", async () => { + const user = userEvent.setup(); + render( + + + + , + ); + + expect(screen.getByText("/gateway-profiles/new")).toBeTruthy(); + await user.click(screen.getByRole("link", { name: "Gateway profile" })); + expect(navigate).toHaveBeenCalledWith("#profile-1"); + }); + + it.each([ + { altKey: true }, + { button: 1 }, + { ctrlKey: true }, + { metaKey: true }, + { shiftKey: true }, + ])("preserves modified browser navigation for %o", (eventInit) => { + render( + + + , + ); + + screen.getByRole("link", { name: "Gateway profile" }).dispatchEvent( + new MouseEvent("click", { + bubbles: true, + cancelable: true, + ...eventInit, + }), + ); + + expect(navigate).not.toHaveBeenCalled(); + }); + + it("rejects consumers without a host provider", () => { + expect(() => render()).toThrow( + "Gateway profile UI must be rendered within GatewayProfileUiProvider", + ); + }); +}); diff --git a/packages/gateway-management-ui/src/gateway-profile-ui-provider.tsx b/packages/gateway-management-ui/src/gateway-profile-ui-provider.tsx new file mode 100644 index 00000000..4340e05b --- /dev/null +++ b/packages/gateway-management-ui/src/gateway-profile-ui-provider.tsx @@ -0,0 +1,78 @@ +import { + createContext, + type MouseEvent, + type PropsWithChildren, + useContext, +} from "react"; + +import type { GatewayProfileOperations } from "./application/gateway-profile-types"; + +export interface GatewayProfileNavigationOptions { + replace?: boolean; + state?: unknown; +} + +export interface GatewayProfileUiNavigation { + collectionHref: string; + createHref: string; + detailHref: (gatewayProfileId: string) => string; + navigate: ( + href: string, + options?: GatewayProfileNavigationOptions, + ) => Promise | void; +} + +export interface GatewayProfileUiServices { + gatewayProfiles: GatewayProfileOperations; + navigation: GatewayProfileUiNavigation; +} + +const GatewayProfileUiContext = createContext< + GatewayProfileUiServices | undefined +>(undefined); + +export function GatewayProfileUiProvider({ + children, + gatewayProfiles, + navigation, +}: PropsWithChildren) { + return ( + + {children} + + ); +} + +export function useGatewayProfileUi(): GatewayProfileUiServices { + const services = useContext(GatewayProfileUiContext); + if (!services) { + throw new Error( + "Gateway profile UI must be rendered within GatewayProfileUiProvider", + ); + } + + return services; +} + +export function useGatewayProfileLink(href: string) { + const { navigation } = useGatewayProfileUi(); + + return { + href, + onClick: (event: MouseEvent) => { + if ( + event.defaultPrevented || + event.button !== 0 || + event.metaKey || + event.ctrlKey || + event.shiftKey || + event.altKey + ) { + return; + } + + event.preventDefault(); + void navigation.navigate(href); + }, + }; +} diff --git a/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-create.test.tsx b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-create.test.tsx new file mode 100644 index 00000000..c5907195 --- /dev/null +++ b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-create.test.tsx @@ -0,0 +1,169 @@ +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { render, screen, waitFor } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { IntlProvider } from "react-intl"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { GatewayProfileUiProvider } from "../gateway-profile-ui-provider"; +import { GatewayProfileCreatePage } from "./gateway-profile-create"; + +const { createGatewayProfileMock, navigateMock } = vi.hoisted(() => ({ + createGatewayProfileMock: vi.fn(), + navigateMock: vi.fn(), +})); + +const gatewayProfileOperations = { + createGatewayProfile: createGatewayProfileMock, + getGatewayProfile: vi.fn(), + listGatewayProfiles: vi.fn(), + removeGatewayProfile: vi.fn(), +}; + +const navigation = { + collectionHref: "/gateway-profiles", + createHref: "/gateway-profiles/new", + detailHref: (gatewayProfileId: string) => + `/gateway-profiles/${gatewayProfileId}`, + navigate: navigateMock, +}; + +const createdGatewayProfile = { + id: "profile-1", + name: "Small profile", +}; + +function renderPage() { + const queryClient = new QueryClient({ + defaultOptions: { + mutations: { retry: false }, + queries: { retry: false }, + }, + }); + + return render( + + + + + + + , + ); +} + +describe("GatewayProfileCreatePage", () => { + beforeEach(() => { + vi.clearAllMocks(); + navigateMock.mockResolvedValue(undefined); + }); + + it("requires a name before sending a request", async () => { + const user = userEvent.setup(); + renderPage(); + + await user.click( + screen.getByRole("button", { name: "Create gateway profile" }), + ); + + expect(await screen.findByText("This field is required.")).toBeTruthy(); + expect(createGatewayProfileMock).not.toHaveBeenCalled(); + }); + + it("creates a profile with only a name and navigates to its detail page", async () => { + const user = userEvent.setup(); + createGatewayProfileMock.mockResolvedValue(createdGatewayProfile); + renderPage(); + + await user.type( + screen.getByRole("textbox", { name: "Profile name" }), + " Small profile ", + ); + await user.click( + screen.getByRole("button", { name: "Create gateway profile" }), + ); + + await waitFor(() => { + expect(createGatewayProfileMock).toHaveBeenCalledWith({ + name: "Small profile", + }); + }); + await waitFor(() => { + expect(navigateMock).toHaveBeenCalledWith("/gateway-profiles/profile-1"); + }); + }); + + it("creates a profile with resource quota values", async () => { + const user = userEvent.setup(); + createGatewayProfileMock.mockResolvedValue(createdGatewayProfile); + renderPage(); + + await user.type( + screen.getByRole("textbox", { name: "Profile name" }), + "Small profile", + ); + await user.type( + screen.getByRole("textbox", { name: "Total CPU request" }), + "4", + ); + await user.type(screen.getByRole("textbox", { name: "Pods" }), "10"); + await user.click( + screen.getByRole("button", { name: "Create gateway profile" }), + ); + + await waitFor(() => { + expect(createGatewayProfileMock).toHaveBeenCalledWith({ + cpuRequestTotal: "4", + name: "Small profile", + podCount: 10, + }); + }); + }); + + it("rejects a non-integer count", async () => { + const user = userEvent.setup(); + renderPage(); + + await user.type( + screen.getByRole("textbox", { name: "Profile name" }), + "Small profile", + ); + await user.type(screen.getByRole("textbox", { name: "Pods" }), "abc"); + await user.click( + screen.getByRole("button", { name: "Create gateway profile" }), + ); + + expect(await screen.findByText("This field is required.")).toBeTruthy(); + expect(createGatewayProfileMock).not.toHaveBeenCalled(); + }); + + it("surfaces a creation error", async () => { + const user = userEvent.setup(); + createGatewayProfileMock.mockRejectedValue(new Error("boom")); + renderPage(); + + await user.type( + screen.getByRole("textbox", { name: "Profile name" }), + "Small profile", + ); + await user.click( + screen.getByRole("button", { name: "Create gateway profile" }), + ); + + expect( + await screen.findByText("Gateway profile could not be created"), + ).toBeTruthy(); + expect(navigateMock).not.toHaveBeenCalled(); + }); + + it("cancels back to the collection", async () => { + const user = userEvent.setup(); + renderPage(); + + await user.click(screen.getByRole("button", { name: "Cancel" })); + + expect(navigateMock).toHaveBeenCalledWith("/gateway-profiles"); + }); +}); diff --git a/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-create.tsx b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-create.tsx new file mode 100644 index 00000000..c1f8a58f --- /dev/null +++ b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-create.tsx @@ -0,0 +1,378 @@ +import { + ActionGroup, + Alert, + Button, + Content, + Form, + FormGroup, + FormHelperText, + HelperText, + HelperTextItem, + PageSection, + TextInput, + Title, +} from "@patternfly/react-core"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useMemo } from "react"; +import { Controller, useForm, type Control } from "react-hook-form"; +import { FormattedMessage, useIntl } from "react-intl"; +import { z } from "zod"; + +import { useGatewayProfileUi } from "../gateway-profile-ui-provider"; +import type { GatewayProfileCreateInput } from "../application/gateway-profile-types"; +import { gatewayProfileMessages } from "../gateway-profile-messages"; +import { messages } from "../messages"; +import { + gatewayProfileListQueryRoot, + gatewayProfileQueryKey, +} from "./gateway-profile-data"; + +export interface GatewayProfileCreatePageProps { + onCreated?: (gatewayProfileId: string) => Promise | void; +} + +interface GatewayProfileFormValues { + containerCpuLimitMax: string; + containerCpuRequestDefault: string; + containerMemoryLimitMax: string; + containerMemoryRequestDefault: string; + cpuLimitTotal: string; + cpuRequestTotal: string; + description: string; + ephemeralStorageTotal: string; + memoryLimitTotal: string; + memoryRequestTotal: string; + name: string; + podCount: string; + pvcCount: string; +} + +type GatewayProfileTextFieldName = keyof GatewayProfileFormValues; + +interface GatewayProfileTextFieldProps { + control: Control< + GatewayProfileFormValues, + undefined, + GatewayProfileCreateInput + >; + fieldId: string; + isDisabled: boolean; + isRequired?: boolean; + label: string; + name: GatewayProfileTextFieldName; +} + +function GatewayProfileTextField({ + control, + fieldId, + isDisabled, + isRequired = false, + label, + name, +}: GatewayProfileTextFieldProps) { + return ( + ( + + { + field.onChange(value); + }} + validated={fieldState.error ? "error" : "default"} + value={field.value} + /> + {fieldState.error ? ( + + + + {fieldState.error.message} + + + + ) : null} + + )} + /> + ); +} + +export function GatewayProfileCreatePage({ + onCreated, +}: GatewayProfileCreatePageProps = {}) { + const intl = useIntl(); + const { gatewayProfiles, navigation } = useGatewayProfileUi(); + const queryClient = useQueryClient(); + const requiredMessage = intl.formatMessage(messages.requiredField); + const schema = useMemo(() => { + const optionalString = z + .string() + .trim() + .transform((value) => (value.length > 0 ? value : undefined)); + const optionalCount = z + .string() + .trim() + .transform((value, context) => { + if (value.length === 0) { + return undefined; + } + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed < 0) { + context.addIssue({ code: "custom", message: requiredMessage }); + return z.NEVER; + } + return parsed; + }); + + return z.object({ + containerCpuLimitMax: optionalString, + containerCpuRequestDefault: optionalString, + containerMemoryLimitMax: optionalString, + containerMemoryRequestDefault: optionalString, + cpuLimitTotal: optionalString, + cpuRequestTotal: optionalString, + description: optionalString, + ephemeralStorageTotal: optionalString, + memoryLimitTotal: optionalString, + memoryRequestTotal: optionalString, + name: z.string().trim().min(1, requiredMessage), + podCount: optionalCount, + pvcCount: optionalCount, + }); + }, [requiredMessage]); + const { control, handleSubmit } = useForm< + GatewayProfileFormValues, + undefined, + GatewayProfileCreateInput + >({ + defaultValues: { + containerCpuLimitMax: "", + containerCpuRequestDefault: "", + containerMemoryLimitMax: "", + containerMemoryRequestDefault: "", + cpuLimitTotal: "", + cpuRequestTotal: "", + description: "", + ephemeralStorageTotal: "", + memoryLimitTotal: "", + memoryRequestTotal: "", + name: "", + podCount: "", + pvcCount: "", + }, + resolver: zodResolver(schema), + }); + + const createGatewayProfile = useMutation({ + mutationFn: (values: GatewayProfileCreateInput) => { + return gatewayProfiles.createGatewayProfile(values); + }, + onSuccess: async (gatewayProfile) => { + queryClient.setQueryData( + gatewayProfileQueryKey(gatewayProfile.id), + gatewayProfile, + ); + await queryClient.invalidateQueries({ + queryKey: gatewayProfileListQueryRoot, + }); + if (onCreated) { + await onCreated(gatewayProfile.id); + } else { + await navigation.navigate(navigation.detailHref(gatewayProfile.id)); + } + }, + }); + + const submit = handleSubmit((values) => { + createGatewayProfile.mutate(values); + }); + + return ( + <> + + + + <FormattedMessage + {...gatewayProfileMessages.createGatewayProfile} + /> + +

+ +

+
+
+ +
void submit(event)} + > + {createGatewayProfile.isError ? ( + + + + ) : null} + + + + + + + + + + + + + + + + + + +
+ + ); +} diff --git a/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-data.test.ts b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-data.test.ts new file mode 100644 index 00000000..c0c8eb9e --- /dev/null +++ b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-data.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; + +import { defaultGatewayProfileListRequest } from "../application/gateway-profile-types"; +import { + gatewayProfileListQueryKey, + gatewayProfileListQueryRoot, + gatewayProfileQueryKey, + gatewayProfileSearchQueryKey, +} from "./gateway-profile-data"; + +describe("gateway profile query keys", () => { + it("builds a stable list query key from the request", () => { + expect( + gatewayProfileListQueryKey(defaultGatewayProfileListRequest), + ).toEqual([...gatewayProfileListQueryRoot, 1, 20, "", "name", "asc"]); + }); + + it("distinguishes list keys by page, size, search, and sort", () => { + const first = gatewayProfileListQueryKey({ + page: 2, + search: "small", + size: 50, + sortDirection: "desc", + sortField: "created", + }); + const second = gatewayProfileListQueryKey(defaultGatewayProfileListRequest); + + expect(first).not.toEqual(second); + }); + + it("builds a detail query key", () => { + expect(gatewayProfileQueryKey("profile-1")).toEqual([ + "gateway-profiles", + "detail", + "profile-1", + ]); + }); + + it("trims the search query key", () => { + expect(gatewayProfileSearchQueryKey(" small ")).toEqual([ + "gateway-profiles", + "search", + "small", + ]); + }); +}); diff --git a/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-data.ts b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-data.ts new file mode 100644 index 00000000..48a279a6 --- /dev/null +++ b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-data.ts @@ -0,0 +1,30 @@ +import type { GatewayProfileListRequest } from "../application/gateway-profile-types"; + +export const gatewayProfileListQueryRoot = [ + "gateway-profiles", + "list", +] as const; +export const gatewayProfileSearchQueryRoot = [ + "gateway-profiles", + "search", +] as const; +export const gatewayProfileSearchDebounceMilliseconds = 250; + +export function gatewayProfileListQueryKey(request: GatewayProfileListRequest) { + return [ + ...gatewayProfileListQueryRoot, + request.page, + request.size, + request.search, + request.sortField, + request.sortDirection, + ] as const; +} + +export function gatewayProfileQueryKey(gatewayProfileId: string) { + return ["gateway-profiles", "detail", gatewayProfileId] as const; +} + +export function gatewayProfileSearchQueryKey(search: string) { + return [...gatewayProfileSearchQueryRoot, search.trim()] as const; +} diff --git a/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-delete-dialog.tsx b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-delete-dialog.tsx new file mode 100644 index 00000000..e987cb41 --- /dev/null +++ b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-delete-dialog.tsx @@ -0,0 +1,136 @@ +import { + Alert, + Button, + Modal, + ModalBody, + ModalFooter, + ModalHeader, + ModalVariant, + Stack, + StackItem, +} from "@patternfly/react-core"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useId } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { GatewayProfileOperationError } from "../application/gateway-profile-types"; +import { useGatewayProfileUi } from "../gateway-profile-ui-provider"; +import { gatewayProfileMessages } from "../gateway-profile-messages"; +import { messages } from "../messages"; +import { + gatewayProfileListQueryRoot, + gatewayProfileQueryKey, +} from "./gateway-profile-data"; + +interface GatewayProfileDeleteDialogProps { + gatewayProfileId: string; + gatewayProfileName: string; + isOpen: boolean; + onClose: () => void; + onDeleted: () => void; +} + +function isConflict(error: unknown): boolean { + return ( + error instanceof GatewayProfileOperationError && error.kind === "conflict" + ); +} + +export function GatewayProfileDeleteDialog({ + gatewayProfileId, + gatewayProfileName, + isOpen, + onClose, + onDeleted, +}: GatewayProfileDeleteDialogProps) { + const intl = useIntl(); + const { gatewayProfiles } = useGatewayProfileUi(); + const queryClient = useQueryClient(); + const descriptionId = useId(); + const titleId = useId(); + const deletion = useMutation({ + mutationFn: () => gatewayProfiles.removeGatewayProfile(gatewayProfileId), + onSuccess: async () => { + queryClient.removeQueries({ + exact: true, + queryKey: gatewayProfileQueryKey(gatewayProfileId), + }); + onDeleted(); + await queryClient.invalidateQueries({ + queryKey: gatewayProfileListQueryRoot, + }); + }, + }); + + const close = () => { + deletion.reset(); + onClose(); + }; + + const conflict = deletion.isError && isConflict(deletion.error); + + return ( + + + + + + + + {deletion.isError ? ( + + + + + + ) : null} + + + + + + + + ); +} diff --git a/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-load-state.tsx b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-load-state.tsx new file mode 100644 index 00000000..e3e1b443 --- /dev/null +++ b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-load-state.tsx @@ -0,0 +1,39 @@ +import { Alert, Bullseye, PageSection, Spinner } from "@patternfly/react-core"; +import { FormattedMessage, useIntl } from "react-intl"; + +import { gatewayProfileMessages } from "../gateway-profile-messages"; + +export function GatewayProfileLoadState({ + isError = false, +}: { + isError?: boolean; +}) { + const intl = useIntl(); + + return ( + + {isError ? ( + + + + ) : ( + + + + )} + + ); +} diff --git a/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-row-actions.tsx b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-row-actions.tsx new file mode 100644 index 00000000..6023c4f6 --- /dev/null +++ b/packages/gateway-management-ui/src/gateway-profiles/gateway-profile-row-actions.tsx @@ -0,0 +1,80 @@ +import { + Dropdown, + DropdownItem, + DropdownList, + MenuToggle, +} from "@patternfly/react-core"; +import { EllipsisVIcon } from "@patternfly/react-icons"; +import { useState } from "react"; +import { FormattedMessage, useIntl } from "react-intl"; + +import type { GatewayProfileRecord } from "../application/gateway-profile-types"; +import { gatewayProfileMessages } from "../gateway-profile-messages"; +import { GatewayProfileDeleteDialog } from "./gateway-profile-delete-dialog"; + +export function GatewayProfileRowActions({ + gatewayProfile, + onDeleted, +}: { + gatewayProfile: GatewayProfileRecord; + onDeleted: () => void; +}) { + const intl = useIntl(); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); + const [isOpen, setIsOpen] = useState(false); + + return ( + <> + { + setIsOpen(false); + }} + popperProps={{ position: "right" }} + shouldFocusToggleOnSelect + toggle={(toggleRef) => ( + { + setIsOpen((open) => !open); + }} + ref={toggleRef} + variant="plain" + > + + )} + > + + { + setIsDeleteOpen(true); + }} + > + + + + + { + setIsDeleteOpen(false); + }} + onDeleted={() => { + setIsDeleteOpen(false); + onDeleted(); + }} + /> + + ); +} diff --git a/packages/gateway-management-ui/src/gateway-ui-provider.test.tsx b/packages/gateway-management-ui/src/gateway-ui-provider.test.tsx index f9390c37..f613d8a4 100644 --- a/packages/gateway-management-ui/src/gateway-ui-provider.test.tsx +++ b/packages/gateway-management-ui/src/gateway-ui-provider.test.tsx @@ -12,6 +12,7 @@ const gatewayOperations = { createOpenShellGatewayServiceAccount: vi.fn(), deleteOpenShellGatewayServiceAccount: vi.fn(), findGatewayPlacements: vi.fn(), + findGatewayProfiles: vi.fn(), getGateway: vi.fn(), getGatewayPlacement: vi.fn(), getGatewayPlacements: vi.fn(), diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx b/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx index 0901361e..ed8e57fd 100644 --- a/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx +++ b/packages/gateway-management-ui/src/gateways/gateway-create.test.tsx @@ -7,17 +7,23 @@ import { vi } from "vitest"; import { GatewayUiProvider } from "../gateway-ui-provider"; import { GatewayCreatePage } from "./gateway-create"; -const { createGatewayMock, findGatewayPlacementsMock, navigateMock } = - vi.hoisted(() => ({ - createGatewayMock: vi.fn(), - findGatewayPlacementsMock: vi.fn(), - navigateMock: vi.fn(), - })); +const { + createGatewayMock, + findGatewayPlacementsMock, + findGatewayProfilesMock, + navigateMock, +} = vi.hoisted(() => ({ + createGatewayMock: vi.fn(), + findGatewayPlacementsMock: vi.fn(), + findGatewayProfilesMock: vi.fn(), + navigateMock: vi.fn(), +})); const gatewayOperations = { createOpenShellGatewayServiceAccount: vi.fn(), deleteOpenShellGatewayServiceAccount: vi.fn(), findGatewayPlacements: findGatewayPlacementsMock, + findGatewayProfiles: findGatewayProfilesMock, getGateway: vi.fn(), getGatewayPlacement: vi.fn(), getGatewayPlacements: vi.fn(), @@ -38,7 +44,7 @@ const navigation = { }; const createdGateway = { - clusterId: "", + clusterId: "cluster-east", databaseId: "", externalDns: "", id: "gateway-1", @@ -82,49 +88,24 @@ describe("GatewayCreatePage", () => { }, ], }); - navigateMock.mockResolvedValue(undefined); - }); - - it("provisions on the hub by default without exposing a namespace", async () => { - const user = userEvent.setup(); - createGatewayMock.mockResolvedValue(createdGateway); - renderPage(); - - expect( - screen.getByRole("combobox", { name: "Cluster" }).value, - ).toBe("Hub cluster (default)"); - expect(screen.queryByLabelText("Namespace")).toBeNull(); - expect(screen.queryByLabelText("Gateway release")).toBeNull(); - expect(screen.queryByLabelText("Managed database")).toBeNull(); - - await user.type( - screen.getByRole("textbox", { name: "Gateway name" }), - " team-gateway ", - ); - await user.click(screen.getByRole("button", { name: "Provision gateway" })); - - await waitFor(() => { - expect(createGatewayMock).toHaveBeenCalledWith({ - clusterId: "", - name: "team-gateway", - }); - }); - await waitFor(() => { - expect(navigateMock).toHaveBeenCalledWith("/gateways/gateway-1"); + findGatewayProfilesMock.mockResolvedValue({ + hasMore: false, + items: [ + { + description: "Small resource quota", + id: "profile-small", + name: "Small profile", + }, + ], }); + navigateMock.mockResolvedValue(undefined); }); it("searches managed clusters and provisions on the selected result", async () => { const user = userEvent.setup(); - createGatewayMock.mockResolvedValue({ - ...createdGateway, - clusterId: "cluster-east", - }); + createGatewayMock.mockResolvedValue(createdGateway); renderPage(); - await user.click( - screen.getByRole("button", { name: "Clear cluster search" }), - ); const clusterInput = screen.getByRole("combobox", { name: "Cluster" }); await user.type(clusterInput, "East"); const clusterOption = await screen.findByText("Cluster East"); @@ -142,6 +123,7 @@ describe("GatewayCreatePage", () => { expect(createGatewayMock).toHaveBeenCalledWith({ clusterId: "cluster-east", name: "team-gateway", + profileId: "profile-small", }); }); expect(findGatewayPlacementsMock).toHaveBeenCalledWith( @@ -161,9 +143,6 @@ describe("GatewayCreatePage", () => { }); findGatewayPlacementsMock.mockClear(); - await user.click( - screen.getByRole("button", { name: "Clear cluster search" }), - ); await user.type(screen.getByRole("combobox", { name: "Cluster" }), "East"); expect(findGatewayPlacementsMock).not.toHaveBeenCalled(); @@ -206,35 +185,6 @@ describe("GatewayCreatePage", () => { expect(findGatewayPlacementsMock).toHaveBeenCalledOnce(); }); - it("keeps hub provisioning available when managed clusters fail to load", async () => { - const user = userEvent.setup(); - findGatewayPlacementsMock.mockRejectedValue( - new Error("managed cluster API unavailable"), - ); - createGatewayMock.mockResolvedValue(createdGateway); - renderPage(); - - expect( - await screen.findByText("Managed clusters could not be loaded"), - ).toBeTruthy(); - expect( - screen.getByRole("combobox", { name: "Cluster" }).value, - ).toBe("Hub cluster (default)"); - expect(screen.getByRole("button", { name: "Retry" })).toBeTruthy(); - - await user.type( - screen.getByRole("textbox", { name: "Gateway name" }), - "team-gateway", - ); - await user.click(screen.getByRole("button", { name: "Provision gateway" })); - await waitFor(() => { - expect(createGatewayMock).toHaveBeenCalledWith({ - clusterId: "", - name: "team-gateway", - }); - }); - }); - it("requires an explicit option after a free-form cluster search", async () => { const user = userEvent.setup(); findGatewayPlacementsMock.mockResolvedValue({ @@ -243,9 +193,6 @@ describe("GatewayCreatePage", () => { }); renderPage(); - await user.click( - screen.getByRole("button", { name: "Clear cluster search" }), - ); await user.type( screen.getByRole("combobox", { name: "Cluster" }), "Unknown placement", @@ -282,9 +229,6 @@ describe("GatewayCreatePage", () => { "More clusters are available. Refine your search to find a specific cluster.", ), ).toBeTruthy(); - await user.click( - screen.getByRole("button", { name: "Clear cluster search" }), - ); const clusterInput = screen.getByRole("combobox", { name: "Cluster", }); @@ -297,15 +241,9 @@ describe("GatewayCreatePage", () => { it("restores the last accepted placement when Escape cancels a search", async () => { const user = userEvent.setup(); - createGatewayMock.mockResolvedValue({ - ...createdGateway, - clusterId: "cluster-east", - }); + createGatewayMock.mockResolvedValue(createdGateway); renderPage(); - await user.click( - screen.getByRole("button", { name: "Clear cluster search" }), - ); const clusterInput = screen.getByRole("combobox", { name: "Cluster", }); @@ -328,6 +266,7 @@ describe("GatewayCreatePage", () => { expect(createGatewayMock).toHaveBeenCalledWith({ clusterId: "cluster-east", name: "team-gateway", + profileId: "profile-small", }); }); }); @@ -397,11 +336,67 @@ describe("GatewayCreatePage", () => { await user.click(screen.getByRole("button", { name: "Provision gateway" })); expect(await screen.findAllByText("This field is required.")).toHaveLength( - 1, + 2, ); expect(createGatewayMock).not.toHaveBeenCalled(); }); + it("provisions with a selected gateway profile", async () => { + const user = userEvent.setup(); + createGatewayMock.mockResolvedValue({ + ...createdGateway, + profileId: "profile-small", + }); + renderPage(); + + await user.click(screen.getByRole("combobox", { name: "Cluster" })); + await user.click(await screen.findByText("Cluster East")); + + const profileInput = screen.getByRole("combobox", { + name: "Profile name", + }); + await user.click(profileInput); + await user.click(await screen.findByText("Small profile")); + expect(profileInput.value).toBe("Small profile"); + + await user.type( + screen.getByRole("textbox", { name: "Gateway name" }), + "team-gateway", + ); + await user.click(screen.getByRole("button", { name: "Provision gateway" })); + + await waitFor(() => { + expect(createGatewayMock).toHaveBeenCalledWith({ + clusterId: "cluster-east", + name: "team-gateway", + profileId: "profile-small", + }); + }); + }); + + it("includes the auto-selected profile by default", async () => { + const user = userEvent.setup(); + createGatewayMock.mockResolvedValue(createdGateway); + renderPage(); + + await user.click(screen.getByRole("combobox", { name: "Cluster" })); + await user.click(await screen.findByText("Cluster East")); + + await user.type( + screen.getByRole("textbox", { name: "Gateway name" }), + "team-gateway", + ); + await user.click(screen.getByRole("button", { name: "Provision gateway" })); + + await waitFor(() => { + expect(createGatewayMock).toHaveBeenCalledWith({ + clusterId: "cluster-east", + name: "team-gateway", + profileId: "profile-small", + }); + }); + }); + it("shows progress only while the create request is pending", async () => { const user = userEvent.setup(); let resolveRequest: ((gateway: typeof createdGateway) => void) | undefined; @@ -413,6 +408,9 @@ describe("GatewayCreatePage", () => { ); renderPage(); + await user.click(screen.getByRole("combobox", { name: "Cluster" })); + await user.click(await screen.findByText("Cluster East")); + await user.type( screen.getByRole("textbox", { name: "Gateway name" }), "team-gateway", diff --git a/packages/gateway-management-ui/src/gateways/gateway-create.tsx b/packages/gateway-management-ui/src/gateways/gateway-create.tsx index 008929cb..a919a277 100644 --- a/packages/gateway-management-ui/src/gateways/gateway-create.tsx +++ b/packages/gateway-management-ui/src/gateways/gateway-create.tsx @@ -13,17 +13,22 @@ import { Title, } from "@patternfly/react-core"; import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; -import { useMemo } from "react"; -import { Controller, useForm, type Control } from "react-hook-form"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect, useMemo } from "react"; +import { Controller, useForm, useWatch, type Control } from "react-hook-form"; import { FormattedMessage, useIntl } from "react-intl"; import { z } from "zod"; import { useGatewayUi } from "../gateway-ui-provider"; import type { GatewayProvisionInput } from "../application/gateway-types"; import { messages } from "../messages"; -import { gatewayListQueryRoot, gatewayQueryKey } from "./gateway-data"; +import { + gatewayListQueryRoot, + gatewayPlacementDetailQueryKey, + gatewayQueryKey, +} from "./gateway-data"; import { GatewayPlacementSelect } from "./gateway-placement-select"; +import { GatewayProfileSelect } from "./gateway-profile-select"; export interface GatewayCreatePageProps { onCreated?: (gatewayId: string) => Promise | void; @@ -32,6 +37,7 @@ export interface GatewayCreatePageProps { interface GatewayFormValues { clusterId: string | null; name: string; + profileId: string | null; } interface GatewayTextFieldProps { @@ -98,27 +104,47 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) { .string({ error: requiredMessage }) .nullable() .transform((value, context) => { - if (value === null) { + if (!value) { context.addIssue({ code: "custom", message: requiredMessage }); return z.NEVER; } return value; }), name: requiredString, + profileId: z + .string() + .nullable() + .transform((value) => value ?? undefined), }); }, [requiredMessage]); - const { control, handleSubmit } = useForm< + const { control, handleSubmit, setValue } = useForm< GatewayFormValues, undefined, GatewayProvisionInput >({ defaultValues: { - clusterId: "", + clusterId: null, name: "", + profileId: null, }, resolver: zodResolver(schema), }); + const clusterId = useWatch({ control, name: "clusterId" }); + const clusterPlacementQuery = useQuery({ + enabled: !!clusterId, + queryFn: ({ signal }) => + gateways.getGatewayPlacement(clusterId ?? "", signal), + queryKey: gatewayPlacementDetailQueryKey(clusterId ?? ""), + }); + + useEffect(() => { + const profileId = clusterPlacementQuery.data?.profileId; + if (profileId) { + setValue("profileId", profileId); + } + }, [clusterPlacementQuery.data, setValue]); + const createGateway = useMutation({ mutationFn: (values: GatewayProvisionInput) => { return gateways.provisionGateway(values); @@ -186,6 +212,18 @@ export function GatewayCreatePage({ onCreated }: GatewayCreatePageProps = {}) { /> )} /> + ( + + )} + /> + } + onStateChange={changeTableState} + pageSizeOptions={gatewayProfileListPageSizes} + renderRowAction={(gatewayProfile) => ( + { + setDeletedGatewayProfileName(gatewayProfile.name); + }} + /> + )} + rows={visiblePage?.items ?? []} + state={tableState} + /> + + + ); +} + +export interface GatewayProfilePageProps { + gatewayProfile?: GatewayProfileRecord; + gatewayProfileId: string; + onDeleted?: (gatewayProfileName: string) => Promise | void; +} + +export function GatewayProfilePage({ + gatewayProfile, + gatewayProfileId, + onDeleted, +}: GatewayProfilePageProps) { + const intl = useIntl(); + const { gatewayProfiles, navigation } = useGatewayProfileUi(); + const [isDeleteOpen, setIsDeleteOpen] = useState(false); + const gatewayProfileQuery = useQuery({ + enabled: gatewayProfile === undefined && gatewayProfileId.length > 0, + queryFn: ({ signal }) => + gatewayProfiles.getGatewayProfile(gatewayProfileId, signal), + queryKey: gatewayProfileQueryKey(gatewayProfileId), + }); + const visibleGatewayProfile = gatewayProfile ?? gatewayProfileQuery.data; + + if (!visibleGatewayProfile && gatewayProfileQuery.isPending) { + return ; + } + if (!visibleGatewayProfile) { + return ; + } + + const quotaFields: readonly { + label: string; + value?: number | string; + }[] = [ + { + label: intl.formatMessage(gatewayProfileMessages.cpuRequestTotal), + value: visibleGatewayProfile.cpuRequestTotal, + }, + { + label: intl.formatMessage(gatewayProfileMessages.cpuLimitTotal), + value: visibleGatewayProfile.cpuLimitTotal, + }, + { + label: intl.formatMessage(gatewayProfileMessages.memoryRequestTotal), + value: visibleGatewayProfile.memoryRequestTotal, + }, + { + label: intl.formatMessage(gatewayProfileMessages.memoryLimitTotal), + value: visibleGatewayProfile.memoryLimitTotal, + }, + { + label: intl.formatMessage(gatewayProfileMessages.ephemeralStorageTotal), + value: visibleGatewayProfile.ephemeralStorageTotal, + }, + { + label: intl.formatMessage( + gatewayProfileMessages.containerCpuRequestDefault, + ), + value: visibleGatewayProfile.containerCpuRequestDefault, + }, + { + label: intl.formatMessage(gatewayProfileMessages.containerCpuLimitMax), + value: visibleGatewayProfile.containerCpuLimitMax, + }, + { + label: intl.formatMessage( + gatewayProfileMessages.containerMemoryRequestDefault, + ), + value: visibleGatewayProfile.containerMemoryRequestDefault, + }, + { + label: intl.formatMessage(gatewayProfileMessages.containerMemoryLimitMax), + value: visibleGatewayProfile.containerMemoryLimitMax, + }, + { + label: intl.formatMessage(gatewayProfileMessages.podCount), + value: visibleGatewayProfile.podCount, + }, + { + label: intl.formatMessage(gatewayProfileMessages.pvcCount), + value: visibleGatewayProfile.pvcCount, + }, + ]; + + return ( + <> + + + + + {visibleGatewayProfile.name} + + + + + + + + + + + + + + + {visibleGatewayProfile.description ?? ( + + )} + + + {quotaFields.map(({ label, value }) => ( + + {label} + + {value === undefined ? ( + + ) : ( + String(value) + )} + + + ))} + + + { + setIsDeleteOpen(false); + }} + onDeleted={() => { + setIsDeleteOpen(false); + if (onDeleted) { + void onDeleted(visibleGatewayProfile.name); + } else { + void navigation.navigate(navigation.collectionHref, { + state: { + deletedGatewayProfileName: visibleGatewayProfile.name, + }, + }); + } + }} + /> + + ); +} diff --git a/packages/gateway-management-ui/src/service-accounts/service-accounts-page.test.tsx b/packages/gateway-management-ui/src/service-accounts/service-accounts-page.test.tsx index 5f00133d..390dfecf 100644 --- a/packages/gateway-management-ui/src/service-accounts/service-accounts-page.test.tsx +++ b/packages/gateway-management-ui/src/service-accounts/service-accounts-page.test.tsx @@ -59,6 +59,7 @@ const operations: GatewayOperations = { createOpenShellGatewayServiceAccount: mocks.create, deleteOpenShellGatewayServiceAccount: mocks.delete, findGatewayPlacements: vi.fn(), + findGatewayProfiles: vi.fn(), getGateway: vi.fn(), getGatewayPlacement: vi.fn(), getGatewayPlacements: vi.fn(), diff --git a/scripts/kind/seed.sh b/scripts/kind/seed.sh index f643a9cb..65b59d3d 100755 --- a/scripts/kind/seed.sh +++ b/scripts/kind/seed.sh @@ -110,6 +110,27 @@ else warn "Could not obtain API token: ${TOKEN_RESP:0:200}" fi +# platform-admin token for GatewayProfile operations (requires platform:admin realm role). +PLATFORM_ADMIN_AUTH_HEADER="" +info "Obtaining platform-admin token..." +for _ in $(seq 1 5); do + PLATFORM_ADMIN_TOKEN_RESP=$(curl -sSk -m 5 -X POST "${KC_TOKEN_URL}" \ + -d "grant_type=password" \ + -d "client_id=hypershell-frontend" \ + -d "username=platform-admin" \ + -d "password=platform-admin" 2>&1 || true) + PLATFORM_ADMIN_TOKEN=$(echo "${PLATFORM_ADMIN_TOKEN_RESP}" | grep -o '"access_token":"[^"]*"' | cut -d'"' -f4 || true) + if [[ -n "${PLATFORM_ADMIN_TOKEN}" ]]; then + PLATFORM_ADMIN_AUTH_HEADER="Authorization: Bearer ${PLATFORM_ADMIN_TOKEN}" + success "Platform-admin token obtained" + break + fi + sleep 2 +done +if [[ -z "${PLATFORM_ADMIN_AUTH_HEADER}" ]]; then + warn "Could not obtain platform-admin token; GatewayProfile seeding will be skipped" +fi + # Helper: POST a JSON resource; prints the response body on success or failure. api_post() { local url="$1" data="$2" @@ -133,6 +154,18 @@ api_get() { ${auth_args[@]+"${auth_args[@]}"} 2>&1 || true } +api_patch() { + local url="$1" data="$2" + local auth_args=() + if [[ -n "${API_AUTH_HEADER}" ]]; then + auth_args=(-H "${API_AUTH_HEADER}") + fi + curl -sS -w "\n%{http_code}" -X PATCH "${url}" \ + -H "Content-Type: application/json" \ + ${auth_args[@]+"${auth_args[@]}"} \ + -d "${data}" 2>&1 || true +} + extract_id() { local resp="$1" if echo "$resp" | grep -q '"kind":"Error"'; then @@ -148,6 +181,7 @@ seed_failed="" CLUSTER_ID="" RELEASE_ID="" DATABASE_ID="" +SMALL_PROFILE_ID="" if [[ -z "${seed_failed}" ]]; then # Check for existing ManagedCluster @@ -157,7 +191,7 @@ if [[ -z "${seed_failed}" ]]; then EXISTING_MC_RESP=$(echo "${EXISTING_MC_RAW}" | sed '$d') if [[ "${EXISTING_MC_HTTP}" == "200" ]]; then - CLUSTER_ID=$(echo "${EXISTING_MC_RESP}" | grep -o '"name":"local-kind"[^}]*"id":"[^"]*"' | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1 || true) + CLUSTER_ID=$(echo "${EXISTING_MC_RESP}" | grep -o '"id":"[^"]*"[^}]*"name":"local-kind"' | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1 || true) if [[ -n "${CLUSTER_ID}" ]]; then success "local-kind ManagedCluster already exists: ${CLUSTER_ID}" fi @@ -180,6 +214,66 @@ if [[ -z "${seed_failed}" ]]; then fi fi +if [[ -z "${seed_failed}" && -n "${PLATFORM_ADMIN_AUTH_HEADER}" ]]; then + header "GatewayProfile Seeding" + + seed_profile() { + local prof_name="$1" prof_body="$2" + info "Creating ${prof_name} GatewayProfile..." >&2 + local raw http resp id + raw=$(curl -sS -w "\n%{http_code}" -X POST "${API_URL}/api/hypershell/v1/gateway_profiles" \ + -H "Content-Type: application/json" \ + -H "${PLATFORM_ADMIN_AUTH_HEADER}" \ + -d "${prof_body}" 2>&1 || true) + http=$(echo "${raw}" | tail -1) + resp=$(echo "${raw}" | sed '$d') + id=$(extract_id "${resp}") + if [[ -n "${id}" ]]; then + success "${prof_name} GatewayProfile created: ${id}" >&2 + printf '%s' "${id}" + return + fi + warn "${prof_name} GatewayProfile creation returned HTTP ${http}; looking up existing..." >&2 + local list_raw list_resp list_http + list_raw=$(curl -sS -w "\n%{http_code}" -X GET "${API_URL}/api/hypershell/v1/gateway_profiles" \ + -H "${PLATFORM_ADMIN_AUTH_HEADER}" 2>&1 || true) + list_http=$(echo "${list_raw}" | tail -1) + list_resp=$(echo "${list_raw}" | sed '$d') + if [[ "${list_http}" == "200" ]]; then + id=$(echo "${list_resp}" | grep -o "\"name\":\"${prof_name}\"[^}]*\"id\":\"[^\"]*\"" | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1 || true) + if [[ -z "${id}" ]]; then + id=$(echo "${list_resp}" | grep -o "\"id\":\"[^\"]*\"[^}]*\"name\":\"${prof_name}\"" | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1 || true) + fi + fi + if [[ -n "${id}" ]]; then + success "${prof_name} GatewayProfile found: ${id}" >&2 + printf '%s' "${id}" + else + warn "${prof_name} GatewayProfile not found" >&2 + fi + } + + SMALL_PROFILE_ID=$(seed_profile "small" \ + '{"name":"small","description":"Small: 2 CPU / 4Gi RAM namespace quota","cpu_request_total":"2","cpu_limit_total":"4","memory_request_total":"2Gi","memory_limit_total":"4Gi","ephemeral_storage_total":"10Gi","pod_count":20,"pvc_count":5,"container_cpu_request_default":"100m","container_cpu_limit_max":"1","container_memory_request_default":"128Mi","container_memory_limit_max":"1Gi"}') + seed_profile "medium" \ + '{"name":"medium","description":"Medium: 4 CPU / 8Gi RAM namespace quota","cpu_request_total":"4","cpu_limit_total":"8","memory_request_total":"4Gi","memory_limit_total":"8Gi","ephemeral_storage_total":"20Gi","pod_count":40,"pvc_count":10,"container_cpu_request_default":"200m","container_cpu_limit_max":"2","container_memory_request_default":"256Mi","container_memory_limit_max":"2Gi"}' >/dev/null + seed_profile "big" \ + '{"name":"big","description":"Big: 8 CPU / 16Gi RAM namespace quota","cpu_request_total":"8","cpu_limit_total":"16","memory_request_total":"8Gi","memory_limit_total":"16Gi","ephemeral_storage_total":"40Gi","pod_count":80,"pvc_count":20,"container_cpu_request_default":"400m","container_cpu_limit_max":"4","container_memory_request_default":"512Mi","container_memory_limit_max":"4Gi"}' >/dev/null + + if [[ -n "${SMALL_PROFILE_ID}" && -n "${CLUSTER_ID}" ]]; then + info "Setting small GatewayProfile as default on ManagedCluster ${CLUSTER_ID}..." + MC_PATCH_RAW=$(api_patch "${API_URL}/api/hypershell/v1/managed_clusters/${CLUSTER_ID}" \ + "{\"profile_id\":\"${SMALL_PROFILE_ID}\"}") + MC_PATCH_HTTP=$(echo "${MC_PATCH_RAW}" | tail -1) + if [[ "${MC_PATCH_HTTP}" == "200" ]]; then + success "ManagedCluster profile_id set to ${SMALL_PROFILE_ID}" + else + MC_PATCH_RESP=$(echo "${MC_PATCH_RAW}" | sed '$d') + warn "Failed to set profile_id on ManagedCluster (HTTP ${MC_PATCH_HTTP}): ${MC_PATCH_RESP:-no response}" + fi + fi +fi + if [[ -z "${seed_failed}" ]]; then # Check for existing GatewayRelease info "Checking for existing dev-release GatewayRelease..." @@ -188,7 +282,7 @@ if [[ -z "${seed_failed}" ]]; then EXISTING_GR_RESP=$(echo "${EXISTING_GR_RAW}" | sed '$d') if [[ "${EXISTING_GR_HTTP}" == "200" ]]; then - RELEASE_ID=$(echo "${EXISTING_GR_RESP}" | grep -o '"name":"dev-release"[^}]*"id":"[^"]*"' | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1 || true) + RELEASE_ID=$(echo "${EXISTING_GR_RESP}" | grep -o '"id":"[^"]*"[^}]*"name":"dev-release"' | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1 || true) if [[ -n "${RELEASE_ID}" ]]; then success "dev-release GatewayRelease already exists: ${RELEASE_ID}" fi @@ -223,7 +317,7 @@ if [[ -z "${seed_failed}" ]]; then EXISTING_MD_RESP=$(echo "${EXISTING_MD_RAW}" | sed '$d') if [[ "${EXISTING_MD_HTTP}" == "200" ]]; then - DATABASE_ID=$(echo "${EXISTING_MD_RESP}" | grep -o '"name":"openshell-db"[^}]*"id":"[^"]*"' | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1 || true) + DATABASE_ID=$(echo "${EXISTING_MD_RESP}" | grep -o '"id":"[^"]*"[^}]*"name":"openshell-db"' | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1 || true) if [[ -n "${DATABASE_ID}" ]]; then success "openshell-db ManagedDatabase already exists: ${DATABASE_ID}" fi @@ -261,7 +355,7 @@ if [[ -z "${seed_failed}" ]]; then EXISTING_GW_RESP=$(echo "${EXISTING_GW_RAW}" | sed '$d') if [[ "${EXISTING_GW_HTTP}" == "200" ]]; then - EXISTING_GW_ID=$(echo "${EXISTING_GW_RESP}" | grep -o '"name":"dev-gateway"[^}]*"id":"[^"]*"' | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1 || true) + EXISTING_GW_ID=$(echo "${EXISTING_GW_RESP}" | grep -o '"id":"[^"]*"[^}]*"name":"dev-gateway"' | grep -o '"id":"[^"]*"' | cut -d'"' -f4 | head -1 || true) if [[ -n "${EXISTING_GW_ID}" ]]; then success "dev-gateway already exists: ${EXISTING_GW_ID}" GATEWAY_ID="${EXISTING_GW_ID}" @@ -276,6 +370,9 @@ if [[ -z "${seed_failed}" ]]; then # Always send database_id; deployment mode uses the empty placeholder. GW_BODY="{\"name\":\"dev-gateway\",\"cluster_id\":\"${CLUSTER_ID}\",\"release_id\":\"${RELEASE_ID}\",\"oidc\":\"${OIDC_JSON}\"" GW_BODY="${GW_BODY},\"database_id\":\"${DATABASE_ID}\"" + if [[ -n "${SMALL_PROFILE_ID}" ]]; then + GW_BODY="${GW_BODY},\"profile_id\":\"${SMALL_PROFILE_ID}\"" + fi GW_BODY="${GW_BODY},\"route\":\"{\\\"enabled\\\":true}\"" GW_BODY="${GW_BODY}}" GW_RAW=$(api_post "${API_URL}/api/hypershell/v1/gateways" "${GW_BODY}") diff --git a/scripts/kind/up.sh b/scripts/kind/up.sh index b5657134..ddf00bb5 100755 --- a/scripts/kind/up.sh +++ b/scripts/kind/up.sh @@ -748,6 +748,7 @@ else "${SCRIPT_DIR}/seed.sh" fi + # --- kubectl port-forward (no cloud-provider-kind fallback) --- if [[ "${CPK_RUNNING}" == "false" ]]; then header "kubectl Port Forwarding" diff --git a/skills/RECONCILE.md b/skills/RECONCILE.md index 7228737a..53bd0213 100644 --- a/skills/RECONCILE.md +++ b/skills/RECONCILE.md @@ -48,15 +48,16 @@ skills/ ## Reconciliation State -**Last analyzed**: 2026-08-31 (Keycloak event-storm KC-ES-W1 complete) -**Spec corpus**: 40 spec files; the coverage table tracks 32 analyzed feature/spec groups after adding OpenShell Gateway Console -**Codebase commit**: working tree (Keycloak event-storm KC-ES-W1 complete) +**Last analyzed**: 2026-09-01 (Gateway Quota GQ-W1 complete - rebased on main) +**Spec corpus**: 41 spec files; the coverage table tracks 33 analyzed feature/spec groups after adding OpenShell Gateway Quota +**Codebase commit**: reconcile-quota (rebased on e4e4e55) ### Coverage Summary | Domain | Specs | Requirements | Present | Partial | Missing | Deferred | Coverage | |--------|-------|-------------|---------|---------|---------|----------|----------| | Platform - Data Model | 1 | 12 | 11 | 1 | 0 | 0 | 96% | +| Platform - Gateway Quota | 1 | 12 | 12 | 0 | 0 | 0 | 100% | | Platform - Control Plane | 1 | 13 | 8 | 1 | 4 | 0 | 65% | | Platform - Gateway (core) | 1 | 18 | 12 | 3 | 3 | 0 | 75% | | Platform - Gateway DB | 1 | 14 | 11 | 0 | 3 | 0 | 79% | @@ -75,7 +76,7 @@ skills/ | Web Console - Architecture | 1 | 28 | 21 | 5 | 2 | 0 | 86% | | Security - RBAC Enforcement | 1 | 13 | 11 | 0 | 0 | 2 | 85% | | Standards | 13 | 0 | 0 | 0 | 0 | 0 | N/A | -| **TOTAL** | **32** | **225** | **176** | **18** | **26** | **5** | **82%** | +| **TOTAL** | **33** | **236** | **187** | **18** | **26** | **5** | **83%** | ### Spec Dependency Order @@ -205,6 +206,28 @@ Layer 7: web-console/architecture (depends on data-model, security, UI | G17 | SSH Payload Delivery | Missing | `internal/openshell/ssh_upload.go` does not exist | - | Future | | G18 | Per-Tenant Gateway API Resource | Missing | Code creates GRPCRoute only; no per-tenant K8s Gateway | - | W8 | +### openshell-gateway-quota.spec.md + +| # | Requirement | Status | Gap | Code Location | Wave | +|---|-------------|--------|-----|---------------|------| +| GQ-1 | GatewayProfile as API resource (CRUD) | Present | New plugin cloned from managedDatabases pattern; 13 fields incl. quota totals, counts, container defaults/maxima | `plugins/gatewayProfiles/` | GQ-W1 ✅ | +| GQ-2 | Field validation (quantities/counts) | Present | `resource.ParseQuantity` for quantities; non-negative counts → HTTP 400 | `plugins/gatewayProfiles/validate.go` | GQ-W1 ✅ | +| GQ-3 | ManagedCluster default profile (`profile_id`) | Present | Added `profile_id` (+ `database_id`) to model/API/proto/migration | `plugins/managedClusters/` | GQ-W1 ✅ | +| GQ-4 | Gateway `profile_id` required at create | Present | Client value → cluster default → HTTP 400 if neither | `plugins/gateways/service.go` | GQ-W1 ✅ | +| GQ-5 | Profile existence check on create/patch | Present | Non-existent profile → HTTP 400 | `plugins/gateways/service.go`, `handler.go` | GQ-W1 ✅ | +| GQ-6 | Profile reassignment (patch), no-clear | Present | PATCH gateway.profile_id allowed; clearing rejected (400) | `plugins/gateways/handler.go` | GQ-W1 ✅ | +| GQ-7 | Deletion protection - cluster reference | Present | HTTP 409 when profile is a cluster default | `plugins/gatewayProfiles/service.go`, `dao.go` | GQ-W1 ✅ | +| GQ-8 | Deletion protection - gateway reference | Present | HTTP 409 when profile referenced by a gateway | `plugins/gatewayProfiles/service.go`, `dao.go` | GQ-W1 ✅ | +| GQ-9 | Control-plane fetches profile (unary gRPC) | Present | `GetGatewayProfile`; fetch failure blocks provisioning; empty profile_id skips quota | `reconciler/reconciler.go` | GQ-W1 ✅ | +| GQ-10 | ResourceQuota + LimitRange per namespace | Present | Update-or-create via `apiequality.Semantic.DeepEqual`; managed labels; delete-when-empty | `gateway/quota.go` | GQ-W1 ✅ | +| GQ-11 | Controller RBAC (resourcequotas/limitranges) | Present | Added to unified ClusterRole in base overlay | `deploy/base/controller-rbac.yaml` | GQ-W1 ✅ | +| GQ-12 | Full-stack surfaces (SDK/CLI/UI) | Present | Go+TS SDK regenerated; CLI CRUD + `--profile-id`; web-console GatewayProfile slice + profile selector | `components/sdk-*`, `components/cli/`, `packages/gateway-management-ui/`, `components/web-console/` | GQ-W1 ✅ | + +**Divergences (D-items) - code deviates from spec/expectation; specs left unmodified per skill contract:** + +- **DQ-1 (data-model.spec.md drift):** The working tree's `specs/platform/data-model.spec.md` was modified (uncommitted) alongside the new quota spec to add `profile_id`/`database_id`. Reconciliation implemented code to match; the spec edit itself was authored outside `/reconcile` (skill contract forbids modifying specs during reconciliation). No code divergence - recorded for provenance. +- **DQ-2 (reassignment convergence timing):** The spec implies a profile reassignment (`PATCH gateway.profile_id`) re-drives quota reconciliation via the existing gateway watch stream. In practice the control-plane's pre-existing **phase gate** (`reconciler.go` ~L1309: skip non-delete events when phase ∈ {Running, Provisioning, Degraded}) means a reassignment on a non-terminal-phase gateway converges on the next seed/retry (controller reconnect via `forceSeedRecovery`, or a phase transition) rather than instantaneously. This matches how *all* gateway spec-field mutations behave today and was **verified live** (quota updated to the new profile after a controller restart). Changing the global phase gate is out of scope; recorded as a known convergence-timing limitation. + ### openshell-gateway-database.spec.md | # | Requirement | Status | Gap | Code Location | Wave | @@ -476,6 +499,45 @@ Layer 7: web-console/architecture (depends on data-model, security, UI **GC-W1 summary:** Added an edge-terminated OpenShift Route for the console, selected readiness by ingress mode, removed inactive console exposures, aligned route-enable semantics, and added custom-host RBAC. The complete control-plane test suite, affected-package race tests, vet, lint, build, Kustomize renders, alignment scan, and independent review passed. +### GQ-W1: Gateway Quota (GatewayProfile) - full stack + +**Scope:** GQ-1 … GQ-12 +**Dependency:** data-model, control-plane, openshell-gateway (core) +**Status:** Complete (backend + control-plane verified live in kind; full-stack UI verified green) + +1. New `gatewayProfiles` plugin (model/dao/service/handler/presenter/grpc/migration/validate/plugin), cloned from `managedDatabases`. +2. Added `profile_id` to Gateway; `profile_id` + `database_id` to ManagedCluster (model/OpenAPI/proto/migration/presenters/handlers). +3. Create-time profile resolution (client → cluster default → 400) and existence checks; patch reassignment with no-clear rule. +4. Two-reference deletion protection (cluster default + gateway) → HTTP 409, distinct messages. +5. Control-plane: unary `GetGatewayProfile` fetch (fetch failure blocks provisioning; empty profile_id skips quota), `ReconcileNamespaceQuota` (ResourceQuota + LimitRange, update-or-create, managed labels, delete-when-empty). +6. Controller RBAC for resourcequotas/limitranges (base + IBM overlay). +7. Regenerated Go+TS SDK and CLI; added CLI gatewayProfile CRUD (delete cmd hand-added, generator preserves reviewed deletes) and `--profile-id` flags; web-console GatewayProfile vertical slice + gateway profile selector. +8. Unit tests for quota spec/limitrange mapping; backend build/vet + OpenAPI contract test + control-plane build/vet/tests pass. +9. Web-console wiring completed: `GatewayProfileUiProvider` mounted in the application shell, gateway-profile breadcrumbs (collection/detail/create) and host navigation added, routes registered (`gateway-profiles`, `/new`, `/:id`) matching `route-contract.json`. `locales/en.json` re-extracted (+47 GatewayProfile messages). Full `pnpm --filter @openshift-online/hypershell-web-console check` green (format, architecture, lint, typecheck, 101 tests / 14 files with coverage thresholds met, i18n:check, production build, Storybook build); reusable `gateway-management-ui` package `check` green (219+ tests). TS SDK dist built for typecheck. + +**GQ-W1 kind proof of execution (2026-08-28, `KIND_NO_SUDO=true`, api-server + control-plane swapped to local builds):** + +| # | Behavior | Result | +|---|----------|--------| +| 1 | Auth: unauth `GET /gateway_profiles` | HTTP 401 | +| 2 | Auth + new endpoint reachable | HTTP 200 | +| 3 | Schema: `gateway_profiles` table + `gateways.profile_id` + `managed_clusters.profile_id/database_id` | present (psql `\d`) | +| 4 | Create valid profile | HTTP 201 | +| 5 | Invalid quantity `"tow"` | HTTP 400 (`invalid quantity for cpu_request_total`) | +| 6 | Negative `pod_count: -3` | HTTP 400 (`must not be negative`) | +| 7 | Cluster create with default `profile_id` | HTTP 201 (echoed) | +| 8 | Delete profile referenced by cluster default | HTTP 409 (`default profile for one or more clusters`) | +| 9 | Gateway create, no cluster default + no profile | HTTP 400 (`profile_id is required`) | +| 10 | Gateway create inherits cluster default | HTTP 201 (profile_id populated) | +| 11 | Gateway create with explicit profile | HTTP 201 | +| 12 | Gateway create with non-existent profile | HTTP 400 (`does not exist`) | +| 13 | Delete profile referenced by gateway | HTTP 409 (`referenced by one or more gateways`) | +| 14 | ResourceQuota + LimitRange created in gateway namespace, matching profile, managed labels, enforcing (`pods 2/10`) | verified via kubectl | +| 15 | Reassignment → quota **updated** to new profile (pods 20/cpu 4/mem 8Gi) | `INFO updated ResourceQuota` after seed reconcile | +| 16 | Empty/legacy profile_id → quota + limitrange **deleted** | `INFO deleted ResourceQuota` / `deleted LimitRange` | + +RBAC gap found during the proof (the live cluster predated the manifest edit) and fixed by applying the updated ClusterRole; manifests already carry the rule. See DQ-2 for reassignment convergence timing. + ### HYPERSHELL-49 OpenShellGatewayServiceAccount waves | Wave | Scope | Status | diff --git a/specs/platform/data-model.spec.md b/specs/platform/data-model.spec.md index f5ff8929..7504a3f7 100644 --- a/specs/platform/data-model.spec.md +++ b/specs/platform/data-model.spec.md @@ -11,10 +11,11 @@ Gateways, clusters, databases, releases, and networks are **top-level resources* Current model: -- **ManagedCluster** - a Kubernetes cluster registered into the platform. Tracks provider, region, API server URL, and a kubeconfig secret reference. +- **ManagedCluster** - a Kubernetes cluster registered into the platform. Tracks provider, region, API server URL, kubeconfig secret reference, and default profile/database assignments for gateways. - **ManagedDatabase** - a database instance provisioned for gateway use. Tracks provider, region, engine type/version, instance class, and a connection secret reference. - **GatewayRelease** - a versioned container image for gateway deployments. Supports rollout strategies with canary percent/duration controls. -- **Gateway** - an API gateway instance deployed onto a specific cluster, using a specific release and database, within an API-assigned namespace. Tracks TLS mode, service type, external DNS, and lifecycle phase. +- **GatewayProfile** - defines resource quota limits (CPU, memory, storage, pod count) for gateway namespaces. Assigned to gateways via cluster defaults. +- **Gateway** - an API gateway instance deployed onto a specific cluster, using a specific release and database, within an API-assigned namespace. Tracks TLS mode, service type, external DNS, lifecycle phase, and assigned quota profile. - **OpenShellGatewayServiceAccount** - a creator-bound automation identity for one Gateway. It stores an OpenShell role and non-secret Keycloak lifecycle metadata. - **GatewayNetwork** - defines network connectivity topology between gateways. Supports tunnel modes and designates a hub gateway for hub-and-spoke or mesh networking. @@ -31,6 +32,8 @@ erDiagram string kubeconfig_secret string status string api_server_url + string profile_id FK + string database_id FK time created_at time updated_at time deleted_at @@ -65,12 +68,33 @@ erDiagram time deleted_at } + GatewayProfile { + string ID PK + string name + string description + string cpu_request_total + string cpu_limit_total + string memory_request_total + string memory_limit_total + string ephemeral_storage_total + int32 pod_count + int32 pvc_count + string container_cpu_request_default + string container_cpu_limit_max + string container_memory_request_default + string container_memory_limit_max + time created_at + time updated_at + time deleted_at + } + Gateway { string ID PK string name string cluster_id FK string release_id FK string database_id FK + string profile_id FK string namespace string image string[] server_dns_names @@ -122,8 +146,11 @@ erDiagram } ManagedCluster ||--o{ Gateway : "hosts" + ManagedCluster }o--o| GatewayProfile : "default_profile" + ManagedCluster }o--o| ManagedDatabase : "default_database" GatewayRelease ||--o{ Gateway : "deployed_as" ManagedDatabase ||--o{ Gateway : "backed_by" + GatewayProfile ||--o{ Gateway : "enforces_quota_on" Gateway ||--o{ OpenShellGatewayServiceAccount : "authorizes" Gateway ||--o| GatewayNetwork : "hub_gateway" ``` @@ -179,6 +206,95 @@ A Gateway SHALL include provisioning configuration fields that the control plane See [`openshell-gateway.spec.md`](./openshell-gateway.spec.md) and its sub-specs for full provisioning details. +### Requirement: GatewayProfile Lifecycle + +The system SHALL support creating, reading, updating, and deleting GatewayProfiles. A GatewayProfile defines resource quota limits applied to gateway namespaces. + +Create, update, and delete SHALL require the `platform:admin` role; read is open to any authenticated caller holding a role binding. See the GatewayProfile Authorization requirement in [`../security/rbac-enforcement.spec.md`](../security/rbac-enforcement.spec.md). + +| Field | Type | Required | Description | +|---|---|---|---| +| `name` | string | Yes | Human-readable profile name | +| `description` | string | No | Profile purpose/intent | +| `cpu_request_total` | string | No | Total CPU requests (e.g., "4", "500m") | +| `cpu_limit_total` | string | No | Total CPU limits | +| `memory_request_total` | string | No | Total memory requests (e.g., "8Gi") | +| `memory_limit_total` | string | No | Total memory limits | +| `ephemeral_storage_total` | string | No | Total ephemeral storage (e.g., "10Gi") | +| `pod_count` | int32 | No | Maximum number of pods | +| `pvc_count` | int32 | No | Maximum number of PVCs | +| `container_cpu_request_default` | string | No | Default CPU request for containers | +| `container_cpu_limit_max` | string | No | Maximum CPU limit for containers | +| `container_memory_request_default` | string | No | Default memory request for containers | +| `container_memory_limit_max` | string | No | Maximum memory limit for containers | + +All quantity fields follow Kubernetes resource quantity format. Zero/empty values are treated as "not set." + +See [`openshell-gateway-quota.spec.md`](./openshell-gateway-quota.spec.md) for full quota enforcement details. + +Profile quantity and count fields SHALL be validated at create and update time (valid Kubernetes resource quantities; non-negative counts); invalid values SHALL be rejected with HTTP 400. A GatewayProfile SHALL NOT be deleted while referenced by a ManagedCluster (`profile_id` default) or a Gateway (`profile_id`); such a delete SHALL be rejected with HTTP 409. See [`openshell-gateway-quota.spec.md`](./openshell-gateway-quota.spec.md) for the validation and deletion-protection requirements. + +#### Scenario: Create GatewayProfile +- GIVEN a valid profile name and quota limits +- WHEN a POST request is made to `/api/hypershell/v1/gateway_profiles` +- THEN a new GatewayProfile is created with a KSUID +- AND the response includes the created GatewayProfile + +#### Scenario: Deletion blocked while referenced +- GIVEN a GatewayProfile referenced by a ManagedCluster default or a Gateway +- WHEN a DELETE request is made for that profile +- THEN the API server SHALL reject it with HTTP 409 + +### Requirement: ManagedCluster Default Assignments + +ManagedCluster SHALL have optional `profile_id` and `database_id` fields that define default assignments for gateways deployed on that cluster. + +| Field | Type | Description | +|---|---|---| +| `profile_id` | string (optional) | Default GatewayProfile for gateways on this cluster. When set, gateways inherit this profile. When empty, gateways have no quota enforcement. | +| `database_id` | string (optional) | Default ManagedDatabase for gateways on this cluster. When set, gateways use this database unless client specifies otherwise. | + +Both fields are mutable via PATCH requests. + +#### Scenario: Assign default profile to cluster +- GIVEN a ManagedCluster and a GatewayProfile +- WHEN a PATCH request sets `profile_id` on the cluster +- THEN new gateways created on that cluster SHALL inherit the profile +- AND existing gateways SHALL retain their current `profile_id` + +#### Scenario: Assign default database to cluster +- GIVEN a ManagedCluster and a ManagedDatabase +- WHEN a PATCH request sets `database_id` on the cluster +- THEN new gateways created on that cluster SHALL use this database by default +- AND clients MAY override by specifying a different `database_id` (subject to validation) + +See [`openshell-gateway-quota.spec.md`](./openshell-gateway-quota.spec.md) for profile assignment behavior and [`openshell-gateway-database.spec.md`](./openshell-gateway-database.spec.md) for database placement strategies. + +### Requirement: Gateway Profile Assignment + +Gateway SHALL have a `profile_id` field that is server-assigned from the cluster's default profile during creation, and **reassignable** afterward via PATCH to change the gateway's quota. + +| Field | Type | Description | +|---|---|---| +| `profile_id` | string | GatewayProfile ID enforced on this gateway's namespace. Server-assigned from `ManagedCluster.profile_id` at creation (client-supplied values on create are ignored). Reassignable via PATCH; a reassigned value is validated to reference an existing GatewayProfile. | + +When `profile_id` is set, the control plane applies ResourceQuota and LimitRange to the gateway namespace. When empty, no quota enforcement occurs. Changing a gateway's quota is done by reassigning `profile_id`, not by editing the profile in place; the control plane does not reconcile GatewayProfile edits. + +#### Scenario: Gateway inherits cluster profile +- GIVEN a ManagedCluster with `profile_id = ""` +- WHEN a client creates a Gateway on that cluster +- THEN the API server SHALL assign `Gateway.profile_id = ""` +- AND the control plane SHALL enforce quota on the gateway namespace + +#### Scenario: Reassign gateway profile +- GIVEN a Gateway with a `profile_id` +- WHEN a PATCH request sets `profile_id` to a different existing GatewayProfile +- THEN the API server SHALL store the new `profile_id` +- AND the control plane SHALL reconcile the gateway toward the new profile's quota +- AND a PATCH referencing a nonexistent profile SHALL be rejected with HTTP 400 + +See [`openshell-gateway-quota.spec.md`](./openshell-gateway-quota.spec.md) for quota enforcement details. + ### Requirement: Gateway Deployment Lifecycle A Gateway SHALL track its deployment lifecycle through the `phase` field. The `status` field SHALL reflect operational health. diff --git a/specs/platform/openshell-gateway-quota.spec.md b/specs/platform/openshell-gateway-quota.spec.md new file mode 100644 index 00000000..b3cc9fa5 --- /dev/null +++ b/specs/platform/openshell-gateway-quota.spec.md @@ -0,0 +1,659 @@ +# OpenShell Gateway Quota Specification + +**Date:** 2026-08-27 +**Status:** Draft +**Parent:** `openshell-gateway.spec.md` - core gateway provisioning +**Related:** `data-model.spec.md` - GatewayProfile entity definition + +--- + +## Purpose + +This specification defines resource quota enforcement for OpenShell gateway namespaces. The control plane applies Kubernetes ResourceQuota and LimitRange objects to gateway namespaces based on GatewayProfile definitions, preventing runaway resource consumption and enforcing tenant-level resource limits. + +--- + +## Architecture + +### Resource Flow + +``` +GatewayProfile (API resource) + │ Defines quota limits: CPU, memory, storage, pod/PVC counts, container defaults + ▼ +ManagedCluster.profile_id (default profile for cluster) + │ Cluster administrator assigns a default profile + ▼ +Gateway.profile_id (client-supplied or cluster default) + │ API server uses client-supplied value, or falls back to cluster's profile_id + ▼ +Control Plane - GatewayReconciler.fetchQuotaConfig() + │ Fetches GatewayProfile via gRPC + │ Translates to QuotaConfig struct + ▼ +Control Plane - ReconcileNamespaceQuota() + │ Creates/updates ResourceQuota (namespace-level limits) + │ Creates/updates LimitRange (container-level defaults/max) + ▼ +Kubernetes - Admission Control + │ Enforces quota on pod creation + │ Injects default requests, rejects over-limit containers +``` + +### Profile Assignment Strategy + +Gateway `profile_id` is **required**. A gateway cannot be created without a profile. The API server resolves it during gateway creation: + +1. Client creates Gateway with `cluster_id`, optionally supplying `profile_id` +2. If `profile_id` is present in the request, the API server honors that value +3. If `profile_id` is absent, the API server looks up `ManagedCluster.profile_id` and assigns it +4. If neither the client nor the cluster provides a `profile_id`, the API server rejects the creation request with HTTP 400 + +After creation, `profile_id` is **immutable to clearing** but **reassignable** to a different existing GatewayProfile via PATCH. Reassignment is validated (the target profile must exist and `profile_id` cannot be set to null or empty), and re-triggers reconciliation for that gateway through the existing gateway watch stream. + +### Changing Quota: Reassignment, Not Profile Reconciliation + +The control plane does **not** watch or reconcile GatewayProfile changes. Editing a GatewayProfile in place does **not** retroactively re-apply to gateways already using it. This is a deliberate design choice: profiles are modified rarely, so maintaining a permanent profile watch and fleet-wide fan-out for a rare operation is not worth the standing cost and concentrated blast radius. + +To change the resource limits a gateway is subject to, reassign the gateway to a different profile: + +1. Create (or select) the target GatewayProfile with the desired limits +2. PATCH each gateway's `profile_id` to the target profile +3. Each PATCH is a gateway update, which the control plane already watches and reconciles - the new quota is applied per gateway + +This makes migrations **progressive** (canary a few gateways, verify, continue), **reversible** (point gateways back at the previous profile, which still exists), and **auditable** (each gateway's `profile_id` records exactly which profile it runs). See `docs/gateway-quota-presentation.md` (Slide 10) for the full tradeoff rationale. + +> Note: Profiles remain fully editable via the API. "Not reconciled" refers only to propagation - an in-place edit is not pushed to running gateways until each is reassigned (or otherwise re-reconciled). + +--- + +## Requirements + +### Requirement: GatewayProfile as API Resource + +GatewayProfile SHALL be a first-class HyperShell resource kind, persisted in PostgreSQL and exposed via REST and gRPC APIs. A GatewayProfile defines resource quota limits that can be applied to multiple gateways. + +Because a GatewayProfile is the enforcement ceiling the control plane applies, creating, updating, and deleting a profile SHALL require the `platform:admin` role. Reading profiles (list and get) SHALL be permitted for any authenticated caller holding a role binding, so that gateway creators and owners can view profiles to assign one. See the GatewayProfile Authorization requirement in [`../security/rbac-enforcement.spec.md`](../security/rbac-enforcement.spec.md). + +**Schema:** + +| Field | Type | Description | +|---|---|---| +| `id` | string | Unique identifier (KSUID) | +| `name` | string | Human-readable name | +| `description` | string (optional) | Profile purpose/intent | +| `cpu_request_total` | string (optional) | Total CPU requests allowed (e.g., "4", "500m") | +| `cpu_limit_total` | string (optional) | Total CPU limits allowed | +| `memory_request_total` | string (optional) | Total memory requests allowed (e.g., "8Gi", "512Mi") | +| `memory_limit_total` | string (optional) | Total memory limits allowed | +| `ephemeral_storage_total` | string (optional) | Total ephemeral storage allowed (e.g., "10Gi") | +| `pod_count` | int32 (optional) | Maximum number of pods | +| `pvc_count` | int32 (optional) | Maximum number of PersistentVolumeClaims | +| `container_cpu_request_default` | string (optional) | Default CPU request injected into containers without explicit request | +| `container_cpu_limit_max` | string (optional) | Maximum CPU limit a container can request | +| `container_memory_request_default` | string (optional) | Default memory request injected into containers | +| `container_memory_limit_max` | string (optional) | Maximum memory limit a container can request | + +All quantity fields follow Kubernetes resource quantity format (e.g., "500m", "2Gi", "10"). + +Zero-value integers (0) are treated as "not set" and omitted from quota enforcement. Empty strings are treated as "not set". + +#### Scenario: Create a GatewayProfile + +- GIVEN a `platform:admin` wants to define a "small" profile +- WHEN they create a GatewayProfile: + ```json + { + "name": "small", + "description": "Small gateway profile for development environments", + "cpu_request_total": "2", + "cpu_limit_total": "4", + "memory_request_total": "4Gi", + "memory_limit_total": "8Gi", + "ephemeral_storage_total": "10Gi", + "pod_count": 10, + "pvc_count": 5, + "container_cpu_request_default": "100m", + "container_cpu_limit_max": "2", + "container_memory_request_default": "128Mi", + "container_memory_limit_max": "4Gi" + } + ``` +- THEN the API server SHALL persist the GatewayProfile +- AND the profile SHALL be available for assignment to ManagedClusters + +--- + +### Requirement: ManagedCluster Default Profile Assignment + +ManagedCluster SHALL have a `profile_id` field that references a GatewayProfile. This field is optional and mutable. + +When set, all gateways created on that cluster inherit the profile. When empty, gateways on that cluster have no quota enforcement. + +#### Scenario: Assign default profile to cluster + +- GIVEN a ManagedCluster exists +- AND a GatewayProfile "small" exists +- WHEN an administrator updates the cluster: + ```json + { + "profile_id": "" + } + ``` +- THEN the API server SHALL store `profile_id` on the ManagedCluster +- AND new gateways created on that cluster SHALL inherit this profile + +#### Scenario: Remove default profile from cluster + +- GIVEN a ManagedCluster has `profile_id` set +- WHEN an administrator updates the cluster to set `profile_id` to null or empty string +- THEN the API server SHALL clear `profile_id` +- AND new gateways created on that cluster SHALL have no quota enforcement + +--- + +### Requirement: Gateway Profile Assignment + +Gateway SHALL have a `profile_id` field that is **required at creation** and **reassignable afterward**. `profile_id` cannot be cleared after a gateway is created. If the client supplies a `profile_id` on create, the API server uses it. If not, the API server falls back to `ManagedCluster.profile_id`. If neither source provides a value, the creation request is rejected with HTTP 400. + +Clients: +- MAY send `profile_id` on create requests to override the cluster default +- MAY send `profile_id` on PATCH requests to reassign the gateway to a different profile +- SHALL NOT send `profile_id = null` or empty string on PATCH; the API server SHALL reject such requests with HTTP 400 +- MAY read `profile_id` from GET/LIST responses + +When `profile_id` is present on PATCH, the API server SHALL validate that the referenced GatewayProfile exists and SHALL reject the request with HTTP 400 if it does not. This prevents a gateway from being assigned a `profile_id` that the control plane cannot fetch (which would block its provisioning). + +The reconciler uses this field to determine which quota to enforce. + +#### Scenario: Create gateway on cluster with default profile + +- GIVEN a ManagedCluster has `profile_id = ""` +- WHEN a client creates a Gateway on that cluster without specifying `profile_id` +- THEN the API server SHALL assign `Gateway.profile_id = ""` +- AND the control plane SHALL enforce the "small" profile quota on the gateway namespace + +#### Scenario: Create gateway on cluster without default profile + +- GIVEN a ManagedCluster has `profile_id = null` +- WHEN a client creates a Gateway on that cluster without supplying `profile_id` +- THEN the API server SHALL reject the request with HTTP 400: "profile_id is required; cluster has no default profile assigned" + +#### Scenario: Create gateway with explicit profile_id override + +- GIVEN a ManagedCluster has `profile_id = ""` +- WHEN a client creates a Gateway on that cluster with `profile_id = ""` +- THEN the API server SHALL honor the client-supplied value +- AND SHALL store `Gateway.profile_id = ""` (cluster default is NOT applied) + +#### Scenario: Reassign a gateway to a different profile + +- GIVEN a Gateway with `profile_id = ""` +- AND a GatewayProfile "large" exists +- WHEN an administrator PATCHes the gateway with `profile_id = ""` +- THEN the API server SHALL store `profile_id = ""` +- AND the control plane SHALL reconcile the gateway and apply the "large" profile quota to its namespace + +#### Scenario: Reassign a gateway to a nonexistent profile + +- GIVEN a Gateway exists +- WHEN an administrator PATCHes the gateway with `profile_id = ""` +- THEN the API server SHALL reject the request with HTTP 400: "gateway profile does not exist" +- AND the gateway's `profile_id` SHALL be unchanged + +#### Scenario: Attempt to clear profile_id via PATCH + +- GIVEN a Gateway with `profile_id` set +- WHEN an administrator PATCHes the gateway with `profile_id = null` or `profile_id = ""` +- THEN the API server SHALL reject the request with HTTP 400: "profile_id cannot be removed from a gateway; reassign to a different profile instead" +- AND the gateway's `profile_id` SHALL be unchanged + +--- + +### Requirement: Control Plane Quota Fetching + +When reconciling a gateway, the control plane SHALL: + +1. Read `gateway.profile_id` from the gRPC watch event +2. If `profile_id` is empty, skip quota enforcement (no profile assigned) +3. If `profile_id` is set, call `GatewayProfileService.GetGatewayProfile` via gRPC +4. If the gRPC call fails (profile not found, network error, timeout), **BLOCK gateway provisioning** +5. Translate the protobuf `GatewayProfile` to a `QuotaConfig` struct +6. Pass `QuotaConfig` to the reconciler + +**Security Rationale:** When a gateway has `profile_id` set, quota enforcement is expected. Proceeding without quota when fetch fails would allow the gateway to run unconstrained, potentially exhausting cluster resources. Failing fast forces operators to fix the profile issue before the gateway can deploy. + +#### Scenario: Fetch quota config successfully + +- GIVEN a Gateway with `profile_id = ""` +- WHEN the GatewayReconciler processes the event +- THEN it SHALL call `GetGatewayProfile("")` +- AND translate the response to `QuotaConfig` +- AND apply ResourceQuota and LimitRange to the gateway namespace + +#### Scenario: Fetch quota config fails - gateway provisioning blocked + +- GIVEN a Gateway with `profile_id = ""` +- WHEN the GatewayReconciler processes the event +- THEN it SHALL call `GetGatewayProfile` and receive an error +- AND it SHALL return an error from the reconciliation: `failed to fetch GatewayProfile : ` +- AND it SHALL NOT provision the gateway workload +- AND the gateway phase SHALL remain "Provisioning" (or transition to "Failed") +- AND reconciliation SHALL retry on the next loop + +#### Scenario: Gateway without profile (profile_id is empty) + +- GIVEN a Gateway with `profile_id = null` (legacy record predating the required-profile policy) +- WHEN the GatewayReconciler processes the event +- THEN it SHALL skip quota fetching entirely +- AND it SHALL proceed with gateway provisioning (no quota enforcement) +- AND the gateway namespace SHALL have no ResourceQuota or LimitRange + +> Note: Under the current API policy, new gateways are always created with a `profile_id`. The empty case exists for backward compatibility with gateways created before this policy was enforced. + +--- + +### Requirement: ResourceQuota Enforcement + +The control plane SHALL create or update a ResourceQuota named `hypershell-gateway-quota` in each gateway namespace when `QuotaConfig` is non-nil. + +The ResourceQuota SHALL enforce: + +| GatewayProfile Field | Kubernetes Resource | +|---|---| +| `cpu_request_total` | `requests.cpu` | +| `cpu_limit_total` | `limits.cpu` | +| `memory_request_total` | `requests.memory` | +| `memory_limit_total` | `limits.memory` | +| `ephemeral_storage_total` | `requests.ephemeral-storage` | +| `pod_count` | `pods` | +| `pvc_count` | `persistentvolumeclaims` | + +Fields with empty string or zero value SHALL be omitted from the ResourceQuota. An empty ResourceQuota (no fields set) SHALL NOT be created. + +The quota object SHALL have labels: +```yaml +labels: + app.kubernetes.io/managed-by: hypershell-control-plane + hypershell.redhat.io/managed: "true" +``` + +The reconciler SHALL use update-or-create semantics: existing ResourceQuota is updated when its spec diverges from desired state. + +#### Scenario: Apply ResourceQuota to gateway namespace + +- GIVEN a Gateway with a profile defining `cpu_request_total = "2"` and `memory_limit_total = "8Gi"` +- WHEN the GatewayReconciler reconciles the gateway +- THEN it SHALL create a ResourceQuota: + ```yaml + apiVersion: v1 + kind: ResourceQuota + metadata: + name: hypershell-gateway-quota + namespace: openshell- + labels: + app.kubernetes.io/managed-by: hypershell-control-plane + hypershell.redhat.io/managed: "true" + spec: + hard: + requests.cpu: "2" + limits.memory: 8Gi + ``` + +#### Scenario: Update ResourceQuota when the gateway is reassigned + +- GIVEN a Gateway namespace has a ResourceQuota with `requests.cpu: "2"` from profile "small" +- WHEN an administrator reassigns the gateway to profile "large" with `cpu_request_total = "4"` +- AND the GatewayReconciler reconciles (triggered by the gateway update) +- THEN it SHALL fetch the "large" profile +- AND update the ResourceQuota to `requests.cpu: "4"` + +#### Scenario: Pod creation exceeds quota + +- GIVEN a Gateway namespace has ResourceQuota with `requests.memory: "4Gi"` +- AND the namespace already has pods requesting `3Gi` +- WHEN a user attempts to create a pod requesting `2Gi` +- THEN Kubernetes admission SHALL reject the pod with: `forbidden: exceeded quota` + +--- + +### Requirement: LimitRange Enforcement + +The control plane SHALL create or update a LimitRange named `hypershell-gateway-limits` in each gateway namespace when `QuotaConfig` has container-level fields set. + +The LimitRange SHALL enforce: + +| GatewayProfile Field | LimitRange Field | Effect | +|---|---|---| +| `container_cpu_request_default` | `defaultRequest.cpu` | Injected into containers without `resources.requests.cpu` | +| `container_memory_request_default` | `defaultRequest.memory` | Injected into containers without `resources.requests.memory` | +| `container_cpu_limit_max` | `max.cpu` | Rejects containers with `resources.limits.cpu` > this value | +| `container_memory_limit_max` | `max.memory` | Rejects containers with `resources.limits.memory` > this value | + +Fields with empty string SHALL be omitted. A LimitRange with no fields SHALL NOT be created. + +The LimitRange SHALL target `type: Container`. + +The reconciler SHALL use update-or-create semantics: existing LimitRange is updated when its spec diverges from desired state. + +#### Scenario: Apply LimitRange to gateway namespace + +- GIVEN a Gateway with a profile defining `container_cpu_request_default = "100m"` and `container_memory_limit_max = "4Gi"` +- WHEN the GatewayReconciler reconciles the gateway +- THEN it SHALL create a LimitRange: + ```yaml + apiVersion: v1 + kind: LimitRange + metadata: + name: hypershell-gateway-limits + namespace: openshell- + labels: + app.kubernetes.io/managed-by: hypershell-control-plane + hypershell.redhat.io/managed: "true" + spec: + limits: + - type: Container + defaultRequest: + cpu: 100m + max: + memory: 4Gi + ``` + +#### Scenario: Container without requests gets defaults + +- GIVEN a Gateway namespace has LimitRange with `defaultRequest.cpu: "100m"` +- WHEN a user creates a pod with a container that has no `resources.requests.cpu` +- THEN Kubernetes admission SHALL inject `requests.cpu: "100m"` into the container + +#### Scenario: Container exceeds max limit + +- GIVEN a Gateway namespace has LimitRange with `max.memory: "4Gi"` +- WHEN a user creates a pod with a container requesting `limits.memory: "8Gi"` +- THEN Kubernetes admission SHALL reject the pod + +--- + +### Requirement: Quota Reconciliation Idempotence + +The control plane SHALL reconcile quota resources idempotently toward the desired state derived from the gateway's current `profile_id`: + +- If the desired ResourceQuota is non-empty and none exists, create +- If the desired ResourceQuota is non-empty and one exists with divergent spec, update +- If the desired ResourceQuota is non-empty and one exists with matching spec, no change +- If the desired ResourceQuota is empty (no fields, or no profile), delete the managed ResourceQuota if one exists +- Same logic for LimitRange + +Deletion SHALL only remove objects the control plane manages (identified by the `hypershell.redhat.io/managed: "true"` label); objects created by other actors SHALL NOT be touched. + +The reconciler SHALL log quota operations at INFO level: +- `INFO created ResourceQuota hypershell-gateway-quota in namespace ` +- `INFO updated ResourceQuota hypershell-gateway-quota in namespace ` +- `INFO deleted ResourceQuota hypershell-gateway-quota in namespace ` +- `INFO created LimitRange hypershell-gateway-limits in namespace ` +- `INFO updated LimitRange hypershell-gateway-limits in namespace ` +- `INFO deleted LimitRange hypershell-gateway-limits in namespace ` + +#### Scenario: Re-reconcile gateway with unchanged quota + +- GIVEN a Gateway namespace has ResourceQuota and LimitRange matching the profile +- WHEN the GatewayReconciler reconciles the gateway again +- THEN it SHALL read existing quota resources +- AND SHALL NOT update them (spec unchanged) +- AND SHALL NOT log creation/update messages + +--- + +### Requirement: Quota Removal When Desired State Is Empty + +When the desired quota for a gateway is empty - because the gateway was reassigned to a profile that omits the corresponding fields, or (for legacy gateways) because `profile_id = null` - the control plane SHALL reconcile toward absence: + +- It SHALL NOT create new quota resources for an empty desired state +- It SHALL delete any existing **managed** ResourceQuota / LimitRange (identified by the `hypershell.redhat.io/managed: "true"` label) whose desired counterpart is now empty + +Because a gateway carries at most one profile at a time and the reconciler always computes the full desired set, removed fields converge to the correct namespace state without manual cleanup. + +#### Scenario: Gateway without profile (legacy) + +- GIVEN a Gateway with `profile_id = null` (legacy record) and no existing managed quota objects +- WHEN the GatewayReconciler reconciles the gateway +- THEN it SHALL NOT create ResourceQuota or LimitRange +- AND the gateway namespace SHALL have no quota enforcement + +#### Scenario: Reassignment to a profile without container fields removes the LimitRange + +- GIVEN a Gateway namespace has a managed LimitRange from a profile with container defaults +- WHEN the gateway is reassigned to a profile with no container-level fields +- AND the GatewayReconciler reconciles +- THEN it SHALL delete the managed LimitRange +- AND SHALL log `INFO deleted LimitRange hypershell-gateway-limits in namespace ` + +--- + +### Requirement: Profile Deletion Protection + +A GatewayProfile SHALL NOT be deleted while it is referenced by either: + +- a **ManagedCluster** via `ManagedCluster.profile_id` (the cluster's default profile), or +- a **Gateway** via `Gateway.profile_id`. + +The API server SHALL enforce this constraint: a delete request for a GatewayProfile SHALL fail with HTTP 409 Conflict if any cluster or gateway references it. Only soft-deleted (non-live) referrers SHALL be ignored. + +**Rationale:** A gateway that references a deleted profile would have `profile_id` set but be unable to fetch the profile, blocking its provisioning (profile assignment implies quota enforcement). A cluster that references a deleted profile would assign a dangling `profile_id` to every new gateway placed on it. Blocking deletion while either kind of referrer exists prevents both failure modes. + +This mirrors the deletion-protection convention used by ManagedDatabase (see `openshell-gateway-database.spec.md`), which likewise blocks deletion while referenced by a cluster default or a gateway. + +#### Scenario: Attempt to delete profile referenced by a cluster + +- GIVEN a GatewayProfile "small" is the default profile for one or more ManagedClusters via `profile_id` +- WHEN an administrator attempts to delete the "small" profile +- THEN the API server SHALL reject the deletion with HTTP 409: "GatewayProfile is the default profile for one or more clusters and cannot be deleted" + +#### Scenario: Attempt to delete profile referenced by gateways + +- GIVEN a GatewayProfile "small" is referenced by 5 gateways via `profile_id` +- WHEN an administrator attempts to delete the "small" profile +- THEN the API server SHALL reject the deletion with HTTP 409: "GatewayProfile is referenced by one or more gateways and cannot be deleted" + +#### Scenario: Delete profile with no references + +- GIVEN a GatewayProfile referenced by no live cluster and no live gateway +- WHEN an administrator deletes the profile +- THEN the API server SHALL delete the GatewayProfile +- AND future gateways SHALL NOT be able to use this profile_id + +--- + +### Requirement: Profile Field Validation + +The API server SHALL validate GatewayProfile quantity and count fields at create and update time, so that invalid values are rejected at the API boundary rather than silently persisted and later failing gateway reconciliation. + +- Each quantity field (`cpu_request_total`, `cpu_limit_total`, `memory_request_total`, `memory_limit_total`, `ephemeral_storage_total`, `container_cpu_request_default`, `container_cpu_limit_max`, `container_memory_request_default`, `container_memory_limit_max`) that is non-empty SHALL parse as a valid, non-negative Kubernetes resource quantity; otherwise the request SHALL be rejected with HTTP 400. Negative totals are rejected because a negative resource quota or limit is meaningless. +- Count fields (`pod_count`, `pvc_count`) SHALL be non-negative; a negative value SHALL be rejected with HTTP 400. +- Empty string and zero remain valid and mean "not set". + +**Rationale:** Without boundary validation, a typo (e.g., `cpu_request_total: "tow"`) is accepted, assigned to gateways, and only fails later in the control plane at `resource.ParseQuantity` time - turning a single bad input into a provisioning failure discoverable only in control-plane logs. + +#### Scenario: Reject profile with an invalid quantity + +- GIVEN an administrator creates a GatewayProfile with `cpu_request_total = "tow"` +- WHEN the API server processes the request +- THEN it SHALL reject the request with HTTP 400 identifying the invalid field + +#### Scenario: Reject profile with a negative count + +- GIVEN an administrator creates a GatewayProfile with `pod_count = -1` +- WHEN the API server processes the request +- THEN it SHALL reject the request with HTTP 400 + +--- + +## Configuration Reference + +No environment variables. Quota enforcement is fully API-driven via GatewayProfile resources. + +--- + +## Configuration Examples + +### Example 1: Small Development Profile + +```json +{ + "name": "dev-small", + "description": "Small profile for development gateways", + "cpu_request_total": "1", + "cpu_limit_total": "2", + "memory_request_total": "2Gi", + "memory_limit_total": "4Gi", + "pod_count": 10, + "container_cpu_request_default": "50m", + "container_memory_request_default": "64Mi" +} +``` + +Resulting ResourceQuota: +```yaml +spec: + hard: + requests.cpu: "1" + limits.cpu: "2" + requests.memory: 2Gi + limits.memory: 4Gi + pods: "10" +``` + +Resulting LimitRange: +```yaml +spec: + limits: + - type: Container + defaultRequest: + cpu: 50m + memory: 64Mi +``` + +### Example 2: Production Profile with Max Limits + +```json +{ + "name": "production", + "description": "Production gateways with strict limits", + "cpu_request_total": "8", + "cpu_limit_total": "16", + "memory_request_total": "16Gi", + "memory_limit_total": "32Gi", + "ephemeral_storage_total": "50Gi", + "pod_count": 50, + "pvc_count": 10, + "container_cpu_request_default": "500m", + "container_cpu_limit_max": "4", + "container_memory_request_default": "512Mi", + "container_memory_limit_max": "8Gi" +} +``` + +Resulting ResourceQuota: +```yaml +spec: + hard: + requests.cpu: "8" + limits.cpu: "16" + requests.memory: 16Gi + limits.memory: 32Gi + requests.ephemeral-storage: 50Gi + pods: "50" + persistentvolumeclaims: "10" +``` + +Resulting LimitRange: +```yaml +spec: + limits: + - type: Container + defaultRequest: + cpu: 500m + memory: 512Mi + max: + cpu: "4" + memory: 8Gi +``` + +### Example 3: Minimal Profile (Pod Count Only) + +```json +{ + "name": "pod-limit-only", + "pod_count": 20 +} +``` + +Resulting ResourceQuota: +```yaml +spec: + hard: + pods: "20" +``` + +No LimitRange is created (no container-level fields). + +--- + +## Operational Procedures + +### Assign a Profile to All Gateways on a Cluster + +1. Create a GatewayProfile: + ```bash + hsctl create gatewayprofile small \ + --cpu-request-total=2 \ + --memory-limit-total=8Gi + ``` + +2. Assign to cluster: + ```bash + hsctl patch managedcluster \ + --profile-id= + ``` + +3. New gateways inherit the profile automatically. Existing gateways retain their old `profile_id`. + +### Change the Quota a Gateway Uses (Reassignment Migration) + +Editing a GatewayProfile in place does **not** re-apply to gateways already using it - the control plane does not reconcile profile changes. To change the limits enforced on running gateways, reassign them to a different profile: + +1. Create the target profile with the desired limits: + ```bash + hsctl create gatewayprofile large \ + --cpu-request-total=4 \ + --memory-limit-total=16Gi + ``` + +2. Reassign gateways to it (do this progressively - canary a few first, verify, then continue): + ```bash + hsctl patch gateway --profile-id= + ``` + +3. Each reassignment is a gateway update; the control plane reconciles that gateway and applies the new quota to its namespace. To roll back, reassign the gateway to the previous profile (which still exists). + +> If you instead edit an existing profile's fields, the change takes effect for a given gateway only when that gateway is next reconciled (e.g. reassigned, or the control plane restarts). Do not rely on in-place edits to retune a running fleet. + +--- + +## Debugging Reference + +| Symptom | Root Cause | Fix | +|---|---|---| +| Pod rejected with "exceeded quota" | Namespace ResourceQuota exceeded | Check `kubectl describe quota -n `, scale down or increase profile limits | +| Pod rejected with "maximum memory limit" | Container limit exceeds LimitRange max | Reduce container `resources.limits.memory` or increase profile `container_memory_limit_max` | +| Gateway has no quota despite profile_id | Profile fetch failed or profile deleted | Check control plane logs, verify profile exists | +| ResourceQuota exists but LimitRange missing | Profile has no container-level fields | Expected behavior, LimitRange only created when container defaults/max are set | +| Edited a profile but a running gateway's quota did not change | By design, profile edits are not reconciled | Reassign the gateway to the (edited or a new) profile: `hsctl patch gateway --profile-id=` | +| Stale quota after reassigning to a smaller profile | Old reconciler left removed limits behind | Fixed: reconciler now removes managed objects when desired state is empty; re-reconcile the gateway | + +--- + +## References + +- [Kubernetes ResourceQuotas](https://kubernetes.io/docs/concepts/policy/resource-quotas/) +- [Kubernetes LimitRanges](https://kubernetes.io/docs/concepts/policy/limit-range/) +- [Resource Quantity Format](https://kubernetes.io/docs/reference/kubernetes-api/common-definitions/quantity/) diff --git a/specs/security/rbac-enforcement.spec.md b/specs/security/rbac-enforcement.spec.md index 7fd737f9..9fac94f2 100644 --- a/specs/security/rbac-enforcement.spec.md +++ b/specs/security/rbac-enforcement.spec.md @@ -102,12 +102,12 @@ Role ||--o{ RoleBinding : "granted_by" ### Permission Matrix -| Role | Gateways | Gateway CRUD | RBAC Grants | OpenShell Mapping | OpenShellGatewayServiceAccounts | -|------|----------|-------------|-------------|-------------------|-----------------| -| `platform:admin` | view all, delete any | view all + delete any | -- | -- | None without a gateway binding | -| `gateway:creator` | create + own gateways | full (as owner) | grant owner/viewer on own gateways | `openshell-admin` on own gateways | Through the resulting owner binding | -| `gateway:owner` | full (one gateway) | full | grant owner/viewer on that gateway | `openshell-admin` on that gateway | Select `openshell-user` or `openshell-admin`. Manage all OpenShellGatewayServiceAccounts on the gateway. | -| `gateway:viewer` | read (one gateway) | read only | -- | `openshell-user` on that gateway | Select only `openshell-user`. Manage only their own OpenShellGatewayServiceAccounts. | +| Role | Gateways | Gateway CRUD | GatewayProfiles | RBAC Grants | OpenShell Mapping | OpenShellGatewayServiceAccounts | +|------|----------|-------------|-----------------|-------------|-------------------|-----------------| +| `platform:admin` | view all, delete any | view all + delete any | full (create/update/delete + read) | -- | -- | None without a gateway binding | +| `gateway:creator` | create + own gateways | full (as owner) | read only | grant owner/viewer on own gateways | `openshell-admin` on own gateways | Through the resulting owner binding | +| `gateway:owner` | full (one gateway) | full | read only | grant owner/viewer on that gateway | `openshell-admin` on that gateway | Select `openshell-user` or `openshell-admin`. Manage all OpenShellGatewayServiceAccounts on the gateway. | +| `gateway:viewer` | read (one gateway) | read only | read only | -- | `openshell-user` on that gateway | Select only `openshell-user`. Manage only their own OpenShellGatewayServiceAccounts. | ### OpenShell Role Bridge @@ -298,7 +298,7 @@ Platform administrators SHALL NOT be able to: The `platform:admin` role is orthogonal to `gateway:creator`, `gateway:owner`, and `gateway:viewer`. A user may hold multiple roles (e.g., `platform:admin` + `gateway:creator`). -In the initial implementation, the `platform:admin` role is **limited to gateway view and delete operations**. It does NOT grant permissions to: +In the initial implementation, the `platform:admin` role is **limited to gateway view and delete operations, plus full management of GatewayProfiles** (see the GatewayProfile Authorization requirement below). It does NOT grant permissions to: - View or modify GatewayNetworks, GatewayReleases, ManagedClusters, or ManagedDatabases - View or modify Users or RoleBindings @@ -360,6 +360,57 @@ Future iterations may expand platform:admin permissions to include these resourc - AND user A becomes `gateway:owner` of the new gateway - AND user A can still view all other gateways via `platform:admin` +### Requirement: GatewayProfile Authorization + +A GatewayProfile defines the resource ceiling (quota) that the control plane enforces +against gateways assigned to it. Because a profile governs enforcement, the authority to +create, modify, or delete profiles SHALL be restricted to platform administrators; +otherwise any caller able to create gateways could mint a profile with an arbitrarily +large quota and defeat enforcement. + +Authorization for `/api/hypershell/v1/gateway_profiles`: + +- **Create** (POST), **Update** (PATCH), **Delete** (DELETE) SHALL require the + `platform:admin` role. +- **Read** (GET list and GET by id) SHALL be permitted for any authenticated caller that + holds at least one role binding, so that `gateway:creator` and `gateway:owner` users can + view profiles in order to assign one to a gateway. + +This is the first resource for which `platform:admin` carries mutation authority; the role +remains view-and-delete-only for gateways (see Platform Admin Global Access). + +The same rule SHALL be enforced on the gRPC `GatewayProfileService`: `CreateGatewayProfile`, +`UpdateGatewayProfile`, and `DeleteGatewayProfile` require `platform:admin`, while +`GetGatewayProfile` and `ListGatewayProfiles` are open to any caller holding a binding. The +enforcement must not be bypassable by switching transport. + +#### Scenario: Platform admin creates a GatewayProfile + +- GIVEN user A has `platform:admin` +- WHEN user A calls `POST /api/hypershell/v1/gateway_profiles` with a valid quota +- THEN the profile is created +- AND the response is 201 Created + +#### Scenario: Gateway creator cannot create a GatewayProfile + +- GIVEN user A has `gateway:creator` but not `platform:admin` +- WHEN user A calls `POST /api/hypershell/v1/gateway_profiles` +- THEN the response is 403 Forbidden + +#### Scenario: Gateway creator can read GatewayProfiles + +- GIVEN user A has `gateway:creator` but not `platform:admin` +- WHEN user A calls `GET /api/hypershell/v1/gateway_profiles` +- THEN the response is 200 OK with the list of profiles + +#### Scenario: Non-admin cannot modify or delete a GatewayProfile + +- GIVEN user A has `gateway:owner` on gw-1 but not `platform:admin` +- WHEN user A calls `PATCH /api/hypershell/v1/gateway_profiles/gp-1` +- THEN the response is 403 Forbidden +- AND WHEN user A calls `DELETE /api/hypershell/v1/gateway_profiles/gp-1` +- THEN the response is 403 Forbidden + ### Requirement: Platform Admin UI Experience The web UI SHALL support `platform:admin` users as defined in `web-console/architecture.spec.md` requirement WEB-ARCH-02 and WEB-UI-03A. @@ -505,7 +556,8 @@ Integration tests SHALL exercise RBAC enforcement with the new four-role model. |----------|-----------| | Keycloak as authority for platform roles | Centralizes role management. Eliminates need for admin-seeding CLI or DB migration. Role changes take effect on next JWT. | | Four roles model | Minimal model that covers the use cases: platform administration (view/delete all), create gateways, own gateways, view gateways. No fleet-scoped RBAC needed. | -| `platform:admin` is view + delete only | Platform admins handle operational tasks (viewing all gateways, cleaning up orphaned resources). Full modification requires ownership to prevent accidental changes. Separation of concerns: visibility ≠ modification authority. | +| `platform:admin` is view + delete only for gateways | Platform admins handle operational tasks (viewing all gateways, cleaning up orphaned resources). Full modification requires ownership to prevent accidental changes. Separation of concerns: visibility ≠ modification authority. | +| `platform:admin` has full CRUD on GatewayProfiles | A profile is the enforcement ceiling the control plane applies. If a `gateway:creator` could create or edit profiles, they could raise their own quota and defeat enforcement. Restricting profile mutation to platform admins keeps the enforcement authority separate from the enforced. Reads stay open to any bound user so creators/owners can assign a profile. | | `platform:admin` orthogonal to `gateway:creator` | A platform admin may or may not create gateways. Roles compose: `platform:admin` + `gateway:creator` allows both operational oversight and resource creation. | | JWT roles synced to DB on every request | DB is the projection, Keycloak is the authority. Revocations in Keycloak take effect immediately. Existing per-gateway bindings are unaffected by platform role changes. | | Service accounts treated identically to users | Control plane gets `gateway:creator` in Keycloak, provisions like any user. No special bypass logic needed. | diff --git a/specs/web-console/architecture.spec.md b/specs/web-console/architecture.spec.md index dc17aa19..c3425d98 100644 --- a/specs/web-console/architecture.spec.md +++ b/specs/web-console/architecture.spec.md @@ -436,21 +436,13 @@ The gateway provisioning form SHALL collect a gateway name and one placement clu The gateway collection and gateway details SHALL display the managed-cluster name for every gateway with a non-empty `cluster_id`. Gateway details SHALL resolve one stable cluster identifier through the managed-cluster API and provide a direct retry action when name resolution fails. The gateway collection SHALL normalize and deduplicate the visible page's non-empty cluster identifiers through one application-owned normalizer, use that same normalized sequence for the batch cache key and API request, resolve it through one bounded managed-cluster list request, and cache that batch for 60 seconds. Managed-cluster typeahead results SHALL be bounded by the application-owned default collection page size. Both experiences SHALL render explicit loading and unavailable states without exposing the raw identifier as a display fallback. Gateways with an empty `cluster_id` SHALL display `Hub cluster` in both experiences. -`Hub cluster (default)` SHALL be the initially selected placement and SHALL represent the cluster that hosts HyperShell. Selecting this option SHALL send an empty `cluster_id`. Selecting a managed cluster SHALL send that cluster's stable identifier as `cluster_id`. Typing text that does not identify a selected option SHALL NOT silently retain or submit a different placement. Pressing Escape during an uncommitted search SHALL close the popup and restore the input and form value to the last accepted placement. +The cluster field SHALL have no initially selected placement. Selecting a managed cluster SHALL send that cluster's stable identifier as `cluster_id`. Typing text that does not identify a selected option SHALL NOT silently retain or submit a different placement. Pressing Escape during an uncommitted search SHALL close the popup and restore the input and form value to the last accepted placement. -The form SHALL NOT collect a Kubernetes namespace, fleet identifier, release identifier, or database identifier. The host adapter SHALL omit namespace because the API server assigns it, and SHALL send empty fleet, release, and database identifiers until those values have a user-facing or reconciler-defined contract. Loading, no-match, partial-page, managed-cluster API failure, retry, and cancellation states SHALL be explicit. A managed-cluster API failure SHALL NOT prevent provisioning to the hub cluster, but the interface SHALL explain that remote placements are unavailable and provide a retry action. +The form SHALL NOT collect a Kubernetes namespace, fleet identifier, release identifier, or database identifier. The host adapter SHALL omit namespace because the API server assigns it, and SHALL send empty fleet, release, and database identifiers until those values have a user-facing or reconciler-defined contract. Loading, no-match, partial-page, managed-cluster API failure, retry, and cancellation states SHALL be explicit. A managed-cluster API failure SHALL explain that remote placements are unavailable and provide a retry action. Managed-cluster search SHALL execute at the API through the gateway application use case and its application-owned port. Search values SHALL be normalized in the query identity, rapid input SHALL be debounced for 250 milliseconds, obsolete requests SHALL be cancelled, and the query SHALL request only the first bounded result page. Placement results SHALL remain fresh for 60 seconds. Cluster list and gateway provisioning requests SHALL retain one correlation context each and publish their required workflow and dependency probes. -**Verification:** Inspect the composed provisioning form, application port, query key, freshness policy, SDK adapter, request payload, and probe recordings. Exercise keyboard and screen-reader selection, Escape from an uncommitted search, rapid and slow search input, literal `%`, `_`, apostrophe, and backslash search values, warm-cache remount, no-match, more-results, slow load, API failure and retry on both provisioning and gateway details, cancellation, hub provisioning, and managed-cluster provisioning. Confirm that one rapid typing burst produces one API search, warm placement data is reused for 60 seconds, the form contains no namespace or cluster-creation controls, the default payload uses `cluster_id: ""` and omits namespace, the response contains the server-assigned namespace, and a selected managed cluster uses its identifier without changing the other reconciler-owned identifiers. - -#### Scenario: Provision on the Hub Cluster - -- GIVEN the provisioning form has loaded -- WHEN the user keeps `Hub cluster (default)` selected and provisions a gateway -- THEN the request SHALL contain an empty `cluster_id` -- AND the request SHALL omit namespace -- AND the created Gateway response SHALL contain the server-assigned namespace +**Verification:** Inspect the composed provisioning form, application port, query key, freshness policy, SDK adapter, request payload, and probe recordings. Exercise keyboard and screen-reader selection, Escape from an uncommitted search, rapid and slow search input, literal `%`, `_`, apostrophe, and backslash search values, warm-cache remount, no-match, more-results, slow load, API failure and retry on both provisioning and gateway details, cancellation, and managed-cluster provisioning. Confirm that one rapid typing burst produces one API search, warm placement data is reused for 60 seconds, the form contains no namespace or cluster-creation controls, a selected managed cluster uses its identifier without changing the other reconciler-owned identifiers, and provisioning without a cluster selection is rejected. #### Scenario: Provision on a Managed Cluster @@ -463,8 +455,7 @@ Managed-cluster search SHALL execute at the API through the gateway application - GIVEN the managed-cluster API request fails - WHEN the provisioning form reports the failure -- THEN `Hub cluster (default)` SHALL remain available for selection -- AND the user SHALL be able to retry loading managed clusters without losing valid gateway form input +- THEN the user SHALL be able to retry loading managed clusters without losing valid gateway form input #### Scenario: Gateway Collection Shows Placement Names @@ -674,9 +665,9 @@ The UI SHALL NOT infer authorization solely from object visibility, guess whethe The API server scopes gateway visibility by per-gateway RoleBindings and the platform:admin global role -- the gateway list returns gateways where the authenticated user has a `gateway:owner` or `gateway:viewer` binding, or ALL gateways if the user has the `platform:admin` role (see [`security/rbac-enforcement.spec.md`](../security/rbac-enforcement.spec.md)). The provisioning action SHALL be available in the primary gateway experience. The gateway provisioning form SHALL NOT collect OIDC configuration -- the control plane auto-provisions Keycloak OIDC clients and populates OIDC fields when a gateway is created. Gateway provisioning requires the `gateway:creator` role (from Keycloak); the creator automatically becomes `gateway:owner` on the new gateway, which provides admin-tier access via the OIDC Role Bridge (see [`platform/openshell-gateway-keycloak.spec.md`](../platform/openshell-gateway-keycloak.spec.md)). -The gateway table SHALL identify an empty `cluster_id` as `Hub cluster` in a sortable Cluster column. The provisioning form SHALL expose the hub as `Hub cluster (default)` and list existing managed clusters as remote placement choices, without offering cluster creation or registration. Hub placement SHALL send an empty `cluster_id`; managed-cluster placement SHALL send the selected cluster identifier. The form SHALL NOT collect `fleet_id`, `release_id`, `database_id`, or `namespace`; it SHALL send the first three identifiers as empty strings and omit namespace because the API server owns its assignment. Preview OIDC and console values MAY support design work, but production builds SHALL NOT use those placeholders as operational defaults. +The gateway table SHALL identify an empty `cluster_id` as `Hub cluster` in a sortable Cluster column. The provisioning form SHALL list existing managed clusters as placement choices, without offering cluster creation or registration, and SHALL require a cluster selection before submission. Managed-cluster placement SHALL send the selected cluster identifier as `cluster_id`. The form SHALL NOT collect `fleet_id`, `release_id`, `database_id`, or `namespace`; it SHALL send the first three identifiers as empty strings and omit namespace because the API server owns its assignment. Preview OIDC and console values MAY support design work, but production builds SHALL NOT use those placeholders as operational defaults. -**Verification:** Run API contract tests and exercise permissions, the initial globally visible gateway list, managed-cluster search and placement, provisioning, valid, empty, unchanged, conflicting, and failed renames from both entry points, confirmed and cancelled deletion from both entry points, deletion failure and duplicate submission, invalid filters, duplicate creates, stale updates, lifecycle transitions, and error mapping through the BFF and UI. Verify that rename sends only the trimmed name; hub placement sends an empty cluster identifier; managed placement sends the selected cluster identifier; the create request omits namespace; the response contains an API-assigned namespace; and the provisioning form does not expose fleet, release, database, or namespace fields. +**Verification:** Run API contract tests and exercise permissions, the initial globally visible gateway list, managed-cluster search and placement, provisioning, valid, empty, unchanged, conflicting, and failed renames from both entry points, confirmed and cancelled deletion from both entry points, deletion failure and duplicate submission, invalid filters, duplicate creates, stale updates, lifecycle transitions, and error mapping through the BFF and UI. Verify that rename sends only the trimmed name; managed placement sends the selected cluster identifier; the create request omits namespace; the response contains an API-assigned namespace; and the provisioning form does not expose fleet, release, database, or namespace fields. ## Initial Delivery Sequence diff --git a/tests/e2e/e2e-openshell.sh b/tests/e2e/e2e-openshell.sh index 707b95d9..bbb56268 100755 --- a/tests/e2e/e2e-openshell.sh +++ b/tests/e2e/e2e-openshell.sh @@ -81,9 +81,15 @@ done # --- Configuration --- CLI=$(get_cli_binary) -GW_NAME="${E2E_GATEWAY_NAME}" +# Per-run suffix so every resource this execution creates is unique and never +# reuses or collides with objects left by a prior run. DNS-label safe (lowercase +# alphanumeric) because the gateway name flows into a namespace and hostname. +E2E_RUN_SUFFIX="$(date +%s | tail -c6)$(printf '%03x' "$((RANDOM % 4096))")" +GW_NAME="${E2E_GATEWAY_NAME}-${E2E_RUN_SUFFIX}" GW_NAMESPACE="" GW_ID="" +E2E_CREATED_CLUSTER_ID="" +E2E_CREATED_PROFILE_ID="" ORPHAN_NS="" ORPHAN_GC_DEADLINE=0 SANDBOX_NAME="" @@ -122,6 +128,28 @@ cleanup() { acquire_oidc_token 2>/dev/null || true api_curl -X DELETE "${API_HOST}/api/hypershell/v1/gateways/${GW_ID}" &>/dev/null || true fi + if [[ "$E2E_SKIP_CLEANUP" != "1" && -n "$E2E_CREATED_CLUSTER_ID" ]]; then + dim " Cleaning up managed cluster ${E2E_CREATED_CLUSTER_ID}..." + acquire_oidc_token 2>/dev/null || true + api_curl -X DELETE "${API_HOST}/api/hypershell/v1/managed_clusters/${E2E_CREATED_CLUSTER_ID}" &>/dev/null || true + fi + # Delete the profile last: it is referenced by the gateway, which is removed above. + # Profile deletion requires platform:admin; acquire that token specifically. + if [[ "$E2E_SKIP_CLEANUP" != "1" && -n "$E2E_CREATED_PROFILE_ID" ]]; then + dim " Cleaning up gateway profile ${E2E_CREATED_PROFILE_ID}..." + _CLEANUP_PADMIN_TOKEN="" + _CLEANUP_PADMIN_RESP=$(curl -sk -X POST \ + "${E2E_OIDC_ISSUER}/protocol/openid-connect/token" \ + --data-urlencode "grant_type=password" \ + --data-urlencode "client_id=${E2E_OIDC_CLIENT_ID}" \ + --data-urlencode "username=${E2E_PLATFORM_ADMIN_USERNAME}" \ + --data-urlencode "password=${E2E_PLATFORM_ADMIN_PASSWORD}" 2>/dev/null || true) + _CLEANUP_PADMIN_TOKEN=$(echo "$_CLEANUP_PADMIN_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || true) + if [[ -n "$_CLEANUP_PADMIN_TOKEN" ]]; then + curl -sk -X DELETE "${API_HOST}/api/hypershell/v1/gateway_profiles/${E2E_CREATED_PROFILE_ID}" \ + -H "Authorization: Bearer ${_CLEANUP_PADMIN_TOKEN}" &>/dev/null || true + fi + fi } trap cleanup EXIT @@ -367,8 +395,9 @@ for gw in data.get('items', []): pass "Gateway already exists: ${GW_NAME} (${GW_ID}, phase=${GW_PHASE})" else # database_id is a required request property but its value is server-owned. - # CNPG placement resolves the sole ManagedDatabase; deployment - # placement ignores the empty placeholder and creates a new dedicated one. + # CNPG placement resolves the gateway's database from its cluster's default + # database_id (ManagedCluster.database_id); deployment placement ignores the + # empty placeholder and creates a new dedicated one. show_cmd "api_curl ${API_HOST}/api/hypershell/v1/managed_databases" E2E_MD_RESP=$(api_curl "${API_HOST}/api/hypershell/v1/managed_databases" 2>/dev/null || true) PREEXISTING_DATABASE_IDS=$(echo "$E2E_MD_RESP" | python3 -c " @@ -398,16 +427,206 @@ print(items[0].get('id', '') if items else '') fi dim " database_id is assigned by ${DB_PROVIDER} placement" + # CNPG placement resolves the gateway's database from its cluster's default + # database_id and rejects a cluster that has none. Deployment placement creates + # a dedicated database on demand and tolerates an unregistered cluster, so it + # keeps the literal below. For CNPG, register a fresh per-run ManagedCluster + # whose database_id points at the fleet's ManagedDatabase, then reference it by + # its server-assigned id (cluster ids are server-generated, so the name is only + # a label). The cleanup trap removes this cluster when cleanup is not skipped. + # Discover the cluster_id. For deployment mode, use the registered cluster + # (kind-up seeds "local-kind"). For CNPG mode a per-run cluster is created + # below and its ID overwrites this value. + show_cmd "api_curl ${API_HOST}/api/hypershell/v1/managed_clusters" + E2E_CLUSTER_ID=$(api_curl "${API_HOST}/api/hypershell/v1/managed_clusters" 2>/dev/null | python3 -c " +import json, sys +try: + data = json.load(sys.stdin) +except Exception: + data = {} +items = data.get('items', []) +# Prefer a cluster named 'local-kind'; otherwise take the first available. +for c in items: + if c.get('name') == 'local-kind': + print(c.get('id', '')); break +else: + print(items[0].get('id', '') if items else '') +" 2>/dev/null || true) + + if [[ -z "$E2E_CLUSTER_ID" ]]; then + fail_test "No ManagedCluster found; cannot create gateway without a valid cluster_id" + exit 1 + fi + dim " Using cluster_id=${E2E_CLUSTER_ID}" + + if [[ "${DB_PROVIDER}" == "cnpg" ]]; then + E2E_CLUSTER_NAME="e2e-cluster-${E2E_RUN_SUFFIX}" + show_cmd "api_curl -X POST ${API_HOST}/api/hypershell/v1/managed_clusters -d '{name: ${E2E_CLUSTER_NAME}}'" + MC_CREATE_BODY=$(E2E_CLUSTER_NAME="$E2E_CLUSTER_NAME" python3 -c " +import json, os +print(json.dumps({ + 'name': os.environ['E2E_CLUSTER_NAME'], + 'provider': 'kind', + # kubeconfig_secret is a required property. This synthetic cluster only exists + # to carry a default database_id for placement; no real kubeconfig is used. + 'kubeconfig_secret': 'e2e-placeholder', +})) +") + MC_CREATE_RESP=$(api_curl -X POST "${API_HOST}/api/hypershell/v1/managed_clusters" \ + -H "Content-Type: application/json" -d "$MC_CREATE_BODY" 2>/dev/null || true) + E2E_CLUSTER_ID=$(echo "$MC_CREATE_RESP" | python3 -c " +import json, sys +try: + d = json.load(sys.stdin) +except Exception: + d = {} +print(d.get('id', '') if d.get('kind') == 'ManagedCluster' else '') +" 2>/dev/null || true) + if [[ -z "$E2E_CLUSTER_ID" ]]; then + fail_test "Could not create ManagedCluster for CNPG gateway placement" + dim " ${MC_CREATE_RESP:0:300}" + exit 1 + fi + # Register for cleanup as soon as it exists so a later failure still removes it. + E2E_CREATED_CLUSTER_ID="$E2E_CLUSTER_ID" + + # Point the cluster's default database at the fleet's CNPG ManagedDatabase so + # placement can resolve it. + show_cmd "api_curl -X PATCH ${API_HOST}/api/hypershell/v1/managed_clusters/${E2E_CLUSTER_ID} -d '{database_id: ${E2E_DATABASE_ID}}'" + MC_PATCH_BODY=$(E2E_DATABASE_ID="$E2E_DATABASE_ID" python3 -c " +import json, os +print(json.dumps({'database_id': os.environ['E2E_DATABASE_ID']})) +") + E2E_CLUSTER_DB=$(api_curl -X PATCH "${API_HOST}/api/hypershell/v1/managed_clusters/${E2E_CLUSTER_ID}" \ + -H "Content-Type: application/json" -d "$MC_PATCH_BODY" 2>/dev/null | \ + python3 -c "import json,sys; print(json.load(sys.stdin).get('database_id',''))" 2>/dev/null || true) + if [[ "$E2E_CLUSTER_DB" != "$E2E_DATABASE_ID" ]]; then + fail_test "Could not set default database_id on ManagedCluster ${E2E_CLUSTER_ID}" + dim " expected ${E2E_DATABASE_ID}, got ${E2E_CLUSTER_DB:-}" + exit 1 + fi + dim " Using cluster_id=${E2E_CLUSTER_ID} (${E2E_CLUSTER_NAME}, default database_id=${E2E_CLUSTER_DB})" + fi + + # Discover the release_id from the API. kind-up seeds a "dev-release" + # GatewayRelease; prefer it, otherwise take the first available. The ID is a + # server-assigned KSUID, so it must be discovered -- never hardcoded. + show_cmd "api_curl ${API_HOST}/api/hypershell/v1/gateway_releases" + E2E_RELEASE_ID=$(api_curl "${API_HOST}/api/hypershell/v1/gateway_releases" 2>/dev/null | python3 -c " +import json, sys +try: + data = json.load(sys.stdin) +except Exception: + data = {} +items = data.get('items', []) +for r in items: + if r.get('name') == 'dev-release': + print(r.get('id', '')); break +else: + print(items[0].get('id', '') if items else '') +" 2>/dev/null || true) + + if [[ -z "$E2E_RELEASE_ID" ]]; then + fail_test "No GatewayRelease found; cannot create gateway without a valid release_id" + exit 1 + fi + dim " Using release_id=${E2E_RELEASE_ID}" + + # Every gateway must resolve a GatewayProfile: the create request carries a + # profile_id, or the target cluster must have a default profile_id. The per-run + # CNPG cluster above has none, and the deployment-mode literal cluster is not a + # registered cluster, so supply profile_id explicitly. kind-up seeds "small", + # "medium", and "big" profiles; reuse one if present, otherwise create a + # per-run profile that the cleanup trap removes. + show_cmd "api_curl ${API_HOST}/api/hypershell/v1/gateway_profiles" + E2E_PROFILE_ID=$(api_curl "${API_HOST}/api/hypershell/v1/gateway_profiles" 2>/dev/null | python3 -c " +import json, sys +try: + data = json.load(sys.stdin) +except Exception: + data = {} +items = data.get('items', []) +# Prefer a profile named 'small'; otherwise take the first available. +for p in items: + if p.get('name') == 'small': + print(p.get('id', '')); break +else: + print(items[0].get('id', '') if items else '') +" 2>/dev/null || true) + + if [[ -z "$E2E_PROFILE_ID" ]]; then + E2E_PROFILE_NAME="e2e-profile-${E2E_RUN_SUFFIX}" + show_cmd "api_curl -X POST ${API_HOST}/api/hypershell/v1/gateway_profiles -d '{name: ${E2E_PROFILE_NAME}, ...}'" + E2E_PROFILE_BODY=$(E2E_PROFILE_NAME="$E2E_PROFILE_NAME" python3 -c " +import json, os +print(json.dumps({ + 'name': os.environ['E2E_PROFILE_NAME'], + 'description': 'e2e namespace quota', + 'cpu_request_total': '2', + 'cpu_limit_total': '4', + 'memory_request_total': '2Gi', + 'memory_limit_total': '4Gi', + 'ephemeral_storage_total': '10Gi', + 'pod_count': 20, + 'pvc_count': 5, + 'container_cpu_request_default': '100m', + 'container_cpu_limit_max': '1', + 'container_memory_request_default': '128Mi', + 'container_memory_limit_max': '1Gi', +})) +") + # GatewayProfile creation requires platform:admin. The default admin token + # carries gateway:creator but not platform:admin, so acquire a dedicated + # platform-admin token for this call. The test's E2E_PLATFORM_ADMIN_USERNAME + # user holds the platform:admin realm role in Keycloak. + _PLATFORM_ADMIN_TOKEN="" + _PLATFORM_ADMIN_TOKEN_RESP=$(curl -sk -X POST \ + "${E2E_OIDC_ISSUER}/protocol/openid-connect/token" \ + --data-urlencode "grant_type=password" \ + --data-urlencode "client_id=${E2E_OIDC_CLIENT_ID}" \ + --data-urlencode "username=${E2E_PLATFORM_ADMIN_USERNAME}" \ + --data-urlencode "password=${E2E_PLATFORM_ADMIN_PASSWORD}" 2>/dev/null || true) + _PLATFORM_ADMIN_TOKEN=$(echo "$_PLATFORM_ADMIN_TOKEN_RESP" | python3 -c "import json,sys; print(json.load(sys.stdin).get('access_token',''))" 2>/dev/null || true) + if [[ -z "$_PLATFORM_ADMIN_TOKEN" ]]; then + fail_test "Could not acquire platform-admin token for GatewayProfile creation" + exit 1 + fi + E2E_PROFILE_RESP=$(curl -sk -X POST "${API_HOST}/api/hypershell/v1/gateway_profiles" \ + -H "Authorization: Bearer ${_PLATFORM_ADMIN_TOKEN}" \ + -H "Content-Type: application/json" -d "$E2E_PROFILE_BODY" 2>/dev/null || true) + E2E_PROFILE_ID=$(echo "$E2E_PROFILE_RESP" | python3 -c " +import json, sys +try: + d = json.load(sys.stdin) +except Exception: + d = {} +print(d.get('id', '') if d.get('kind') == 'GatewayProfile' else '') +" 2>/dev/null || true) + if [[ -z "$E2E_PROFILE_ID" ]]; then + fail_test "Could not create GatewayProfile for gateway namespace quota" + dim " ${E2E_PROFILE_RESP:0:300}" + exit 1 + fi + # Register for cleanup as soon as it exists so a later failure still removes it. + E2E_CREATED_PROFILE_ID="$E2E_PROFILE_ID" + dim " Created GatewayProfile ${E2E_PROFILE_ID} (${E2E_PROFILE_NAME})" + else + dim " Using existing GatewayProfile ${E2E_PROFILE_ID}" + fi + show_cmd "api_curl -X POST ${API_HOST}/api/hypershell/v1/gateways -d '{name: ${GW_NAME}, database_id: , oidc: ...}'" GW_CREATE_BODY=$(GW_NAME="$GW_NAME" E2E_OIDC_ISSUER="$E2E_OIDC_ISSUER" \ E2E_OIDC_CLIENT_ID="$E2E_OIDC_CLIENT_ID" \ - E2E_DATABASE_ID="$E2E_DATABASE_ID" python3 -c " + E2E_DATABASE_ID="$E2E_DATABASE_ID" \ + E2E_PROFILE_ID="$E2E_PROFILE_ID" E2E_RELEASE_ID="$E2E_RELEASE_ID" \ + E2E_CLUSTER_ID="$E2E_CLUSTER_ID" python3 -c " import json, os body = { 'name': os.environ['GW_NAME'], - 'cluster_id': 'e2e-cluster', - 'release_id': 'e2e-release', + 'cluster_id': os.environ['E2E_CLUSTER_ID'], + 'release_id': os.environ['E2E_RELEASE_ID'], 'database_id': os.environ['E2E_DATABASE_ID'], + 'profile_id': os.environ['E2E_PROFILE_ID'], 'oidc': json.dumps({ 'issuer': os.environ['E2E_OIDC_ISSUER'], 'audience': os.environ['E2E_OIDC_CLIENT_ID'],