diff --git a/go.mod b/go.mod index 0dd2b21b6..dc4e0b89f 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/nais/api/pkg/apiclient v0.0.0-20250219111538-2b76a0fd6ed9 github.com/nais/bifrost v0.0.0-20260106105449-911627ac2c61 github.com/nais/liberator v0.0.0-20260216142648-ee49a9372bc4 - github.com/nais/pgrator/pkg/api v0.0.0-20260219115817-cf954d58c04e + github.com/nais/pgrator/pkg/api v0.0.0-20260528100930-7dcc162c09d6 github.com/nais/tester v0.1.1 github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe github.com/nais/v13s/pkg/api v0.0.0-20260528080657-d4f49e5737da @@ -82,12 +82,12 @@ require ( google.golang.org/api v0.280.0 google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 google.golang.org/grpc v1.81.1 - google.golang.org/protobuf v1.36.11 - k8s.io/api v0.35.1 - k8s.io/apimachinery v0.35.3 - k8s.io/client-go v0.35.1 + google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af + k8s.io/api v0.36.1 + k8s.io/apimachinery v0.36.1 + k8s.io/client-go v0.36.1 k8s.io/klog/v2 v2.140.0 - k8s.io/utils v0.0.0-20260108192941-914a6e750570 + k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 sigs.k8s.io/yaml v1.6.0 ) @@ -170,7 +170,7 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/coreos/go-semver v0.3.1 // indirect - github.com/coreos/go-systemd/v22 v22.6.0 // indirect + github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/cubicdaiya/gonp v1.0.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect @@ -411,9 +411,9 @@ require ( github.com/zitadel/logging v0.6.1 // indirect github.com/zitadel/schema v1.3.0 // indirect go.etcd.io/bbolt v1.4.3 // indirect - go.etcd.io/etcd/api/v3 v3.6.7 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.6.7 // indirect - go.etcd.io/etcd/client/v3 v3.6.7 // indirect + go.etcd.io/etcd/api/v3 v3.6.8 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.6.8 // indirect + go.etcd.io/etcd/client/v3 v3.6.8 // indirect go.mongodb.org/mongo-driver v1.17.9 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect @@ -472,13 +472,13 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect honnef.co/go/tools v0.7.0 // indirect - k8s.io/apiextensions-apiserver v0.35.0 // indirect - k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e // indirect + k8s.io/apiextensions-apiserver v0.36.0 // indirect + k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a // indirect mvdan.cc/gofumpt v0.9.2 // indirect - sigs.k8s.io/controller-runtime v0.23.1 // indirect + sigs.k8s.io/controller-runtime v0.24.1 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.2 // indirect ) replace github.com/GoogleCloudPlatform/k8s-config-connector/mockgcp => ./mockgcp diff --git a/go.sum b/go.sum index 08b8ab991..7ec8a6e58 100644 --- a/go.sum +++ b/go.sum @@ -277,8 +277,8 @@ github.com/coreos/go-oidc/v3 v3.12.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDh github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4= github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= -github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= +github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= +github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -515,8 +515,8 @@ 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/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= -github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef h1:xpF9fUHpoIrrjX24DURVKiwHcFpw19ndIs+FwTSMbno= -github.com/google/pprof v0.0.0-20260202012954-cb029daf43ef/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936 h1:EwtI+Al+DeppwYX2oXJCETMO23COyaKGP6fHVpkpWpg= +github.com/google/pprof v0.0.0-20260402051712-545e8a4df936/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI= github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= @@ -807,8 +807,8 @@ github.com/nais/bifrost v0.0.0-20260106105449-911627ac2c61 h1:DMIjq7U47OJ8GlgOR3 github.com/nais/bifrost v0.0.0-20260106105449-911627ac2c61/go.mod h1:sAeomjrnGAI9VAErCaOHbTehVkf6hhKoJpHL8uzOqGg= github.com/nais/liberator v0.0.0-20260216142648-ee49a9372bc4 h1:i7jBukqLtNpQIBhy/YBA8XjLRVdI8B7WxC9nhQyxlWE= github.com/nais/liberator v0.0.0-20260216142648-ee49a9372bc4/go.mod h1:jmMoQtUMhvv7j1C2gz89Gxc4hxc73GXtTR3mEXn7cvU= -github.com/nais/pgrator/pkg/api v0.0.0-20260219115817-cf954d58c04e h1:FWPwIgFlNjA0akEUt2y6Hp5e03rGg1QmWl56XvnctYs= -github.com/nais/pgrator/pkg/api v0.0.0-20260219115817-cf954d58c04e/go.mod h1:iDLbi5Ss8Fs6L5+ot8jBLStR5/bdiPgblt7OsN06n50= +github.com/nais/pgrator/pkg/api v0.0.0-20260528100930-7dcc162c09d6 h1:xu68LxWvXcV9QUs6vCk3p4SrRyH/0veByRP7vRmw9ZI= +github.com/nais/pgrator/pkg/api v0.0.0-20260528100930-7dcc162c09d6/go.mod h1:wFRwI8RMC1RzNBABV1SOojhVhnEG7C071BcOOayExtE= github.com/nais/tester v0.1.1 h1:tpJ5HKpu3mEIWX/mec0Yj0xLHEpt+MwTAsj282n0Py0= github.com/nais/tester v0.1.1/go.mod h1:NCQMcgftHz/EXorob1XwDTOqkQmImDqr51YQ2Uea9Pc= github.com/nais/unleasherator v0.0.0-20251216221129-efebc54203fe h1:CdRVopOihru4tXVwKZjhg6C8SbPLCQYOhJKpjBZYhjg= @@ -830,11 +830,11 @@ github.com/oklog/ulid/v2 v2.1.1/go.mod h1:rcEKHmBBKfef9DhnvX7y1HZBYxjXb0cP5ExxNs github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo/v2 v2.28.2 h1:DTrMfpqxiNUyQ3Y0zhn1n3cOO2euFgQPYIpkWwxVFps= -github.com/onsi/ginkgo/v2 v2.28.2/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE= +github.com/onsi/ginkgo/v2 v2.29.0 h1:rfh+ZFjgJhYWRoIqVf3Uwx/W20yLrcrE2h2GmYVRaag= +github.com/onsi/ginkgo/v2 v2.29.0/go.mod h1:+aXOY+vzZ5mu2iI2HpTZUPmM//oQfsNFX6gU9kNcA44= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28= -github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg= +github.com/onsi/gomega v1.41.0 h1:OwKp4pXNgVxf6sCplzYo794OFNuoL2q2SBMU5NSWOjA= +github.com/onsi/gomega v1.41.0/go.mod h1:M/Uqpu/8qTjtzCLUA2zJHX9Iilrau25x1PdoSRbWh5A= github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0 h1:0dYiJ7krIwaHFX6YLNDo/yawTZIu8X16tT/nwW1UTG8= github.com/open-telemetry/opentelemetry-collector-contrib/internal/exp/metrics v0.145.0/go.mod h1:mhoa9lipcEH0heeKf6+xHzGUrCuAgImQv4/Qpmu0+Fk= github.com/open-telemetry/opentelemetry-collector-contrib/pkg/pdatautil v0.145.0 h1:sB4yuYx45zig1ceQ+kmrEYy0xMZ+mGagwYIFtJkkU1w= @@ -1122,12 +1122,12 @@ go.einride.tech/aip v0.79.0 h1:19zdPlZzlUvxOA8syAFw4LkdJdXepzyTl6gt9XEeqdU= go.einride.tech/aip v0.79.0/go.mod h1:E8+wdTApA70odnpFzJgsGogHozC2JCIhFJBKPr8bVig= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= -go.etcd.io/etcd/api/v3 v3.6.7 h1:7BNJ2gQmc3DNM+9cRkv7KkGQDayElg8x3X+tFDYS+E0= -go.etcd.io/etcd/api/v3 v3.6.7/go.mod h1:xJ81TLj9hxrYYEDmXTeKURMeY3qEDN24hqe+q7KhbnI= -go.etcd.io/etcd/client/pkg/v3 v3.6.7 h1:vvzgyozz46q+TyeGBuFzVuI53/yd133CHceNb/AhBVs= -go.etcd.io/etcd/client/pkg/v3 v3.6.7/go.mod h1:2IVulJ3FZ/czIGl9T4lMF1uxzrhRahLqe+hSgy+Kh7Q= -go.etcd.io/etcd/client/v3 v3.6.7 h1:9WqA5RpIBtdMxAy1ukXLAdtg2pAxNqW5NUoO2wQrE6U= -go.etcd.io/etcd/client/v3 v3.6.7/go.mod h1:2XfROY56AXnUqGsvl+6k29wrwsSbEh1lAouQB1vHpeE= +go.etcd.io/etcd/api/v3 v3.6.8 h1:gqb1VN92TAI6G2FiBvWcqKtHiIjr4SU2GdXxTwyexbM= +go.etcd.io/etcd/api/v3 v3.6.8/go.mod h1:qyQj1HZPUV3B5cbAL8scG62+fyz5dSxxu0w8pn28N6Q= +go.etcd.io/etcd/client/pkg/v3 v3.6.8 h1:Qs/5C0LNFiqXxYf2GU8MVjYUEXJ6sZaYOz0zEqQgy50= +go.etcd.io/etcd/client/pkg/v3 v3.6.8/go.mod h1:GsiTRUZE2318PggZkAo6sWb6l8JLVrnckTNfbG8PWtw= +go.etcd.io/etcd/client/v3 v3.6.8 h1:B3G76t1UykqAOrbio7s/EPatixQDkQBevN8/mwiplrY= +go.etcd.io/etcd/client/v3 v3.6.8/go.mod h1:MVG4BpSIuumPi+ELF7wYtySETmoTWBHVcDoHdVupwt8= go.mongodb.org/mongo-driver v1.17.9 h1:IexDdCuuNJ3BHrELgBlyaH9p60JXAvdzWR128q+U5tU= go.mongodb.org/mongo-driver v1.17.9/go.mod h1:LlOhpH5NUEfhxcAwG0UEkMqwYcc4JU18gtCdGudk/tQ= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= @@ -1450,8 +1450,8 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI= +google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1489,20 +1489,20 @@ honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.7.0 h1:w6WUp1VbkqPEgLz4rkBzH/CSU6HkoqNLp6GstyTx3lU= honnef.co/go/tools v0.7.0/go.mod h1:pm29oPxeP3P82ISxZDgIYeOaf9ta6Pi0EWvCFoLG2vc= -k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= -k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= -k8s.io/apiextensions-apiserver v0.35.0 h1:3xHk2rTOdWXXJM+RDQZJvdx0yEOgC0FgQ1PlJatA5T4= -k8s.io/apiextensions-apiserver v0.35.0/go.mod h1:E1Ahk9SADaLQ4qtzYFkwUqusXTcaV2uw3l14aqpL2LU= -k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= -k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= -k8s.io/client-go v0.35.1 h1:+eSfZHwuo/I19PaSxqumjqZ9l5XiTEKbIaJ+j1wLcLM= -k8s.io/client-go v0.35.1/go.mod h1:1p1KxDt3a0ruRfc/pG4qT/3oHmUj1AhSHEcxNSGg+OA= +k8s.io/api v0.36.1 h1:XbL/EMj8K2aJpJtePmqUyQMsM0D4QI2pvl7YKJ20FTY= +k8s.io/api v0.36.1/go.mod h1:KOWo4ey3TINlXjeHVuwB3i+tXXnu+UcwFBHlI/9dvEo= +k8s.io/apiextensions-apiserver v0.36.0 h1:Wt7E8J+VBCbj4FjiBfDTK/neXDDjyJVJc7xfuOHImZ0= +k8s.io/apiextensions-apiserver v0.36.0/go.mod h1:kGDjH0msuiIB3tgsYRV0kS9GqpMYMUsQ3GHv7TApyug= +k8s.io/apimachinery v0.36.1 h1:G63Gjx2W+q0YD+72Vo8oY0nDnePVwnuzTmmy5ENrVSA= +k8s.io/apimachinery v0.36.1/go.mod h1:ibYOR00vW/I1kzvi5SF0dRuJ52BvKtfvRdOn35GPQ+8= +k8s.io/client-go v0.36.1 h1:FN/K8QIT2CEDt+2WB2HnWrUANZ50AP5GII43/SP2JR0= +k8s.io/client-go v0.36.1/go.mod h1:s6rAnCtTGYDQnpNjEhSaISV+2O8jwruZ6m3QOYBFbtU= k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc= k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0= -k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e h1:iW9ChlU0cU16w8MpVYjXk12dqQ4BPFBEgif+ap7/hqQ= -k8s.io/kube-openapi v0.0.0-20251125145642-4e65d59e963e/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/utils v0.0.0-20260108192941-914a6e750570 h1:JT4W8lsdrGENg9W+YwwdLJxklIuKWdRm+BC+xt33FOY= -k8s.io/utils v0.0.0-20260108192941-914a6e750570/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a h1:xCeOEAOoGYl2jnJoHkC3hkbPJgdATINPMAxaynU2Ovg= +k8s.io/kube-openapi v0.0.0-20260317180543-43fb72c5454a/go.mod h1:uGBT7iTA6c6MvqUvSXIaYZo9ukscABYi2btjhvgKGZ0= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2 h1:AZYQSJemyQB5eRxqcPky+/7EdBj0xi3g0ZcxxJ7vbWU= +k8s.io/utils v0.0.0-20260210185600-b8788abfbbc2/go.mod h1:xDxuJ0whA3d0I4mf/C4ppKHxXynQ+fxnkmQH0vTHnuk= modernc.org/libc v1.68.0 h1:PJ5ikFOV5pwpW+VqCK1hKJuEWsonkIJhhIXyuF/91pQ= modernc.org/libc v1.68.0/go.mod h1:NnKCYeoYgsEqnY3PgvNgAeaJnso968ygU8Z0DxjoEc0= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= @@ -1513,13 +1513,13 @@ modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU= modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA= mvdan.cc/gofumpt v0.9.2 h1:zsEMWL8SVKGHNztrx6uZrXdp7AX8r421Vvp23sz7ik4= mvdan.cc/gofumpt v0.9.2/go.mod h1:iB7Hn+ai8lPvofHd9ZFGVg2GOr8sBUw1QUWjNbmIL/s= -sigs.k8s.io/controller-runtime v0.23.1 h1:TjJSM80Nf43Mg21+RCy3J70aj/W6KyvDtOlpKf+PupE= -sigs.k8s.io/controller-runtime v0.23.1/go.mod h1:B6COOxKptp+YaUT5q4l6LqUJTRpizbgf9KSRNdQGns0= +sigs.k8s.io/controller-runtime v0.24.1 h1:miPEwrmirImAvgME1L9qebGHrOnGJoVmVdtOU9fRfo4= +sigs.k8s.io/controller-runtime v0.24.1/go.mod h1:vFkfY5fGt5xAC/sKb8IBFKgWPNKG9OUG29dR8Y2wImw= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 h1:2WOzJpHUBVrrkDjU4KBT8n5LDcj824eX0I5UKcgeRUs= -sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2 h1:kwVWMx5yS1CrnFWA/2QHyRVJ8jM6dBA80uLmm0wJkk8= +sigs.k8s.io/structured-merge-diff/v6 v6.3.2/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= diff --git a/integration_tests/k8s_resources/opensearch_crud/dev/someteamname/opensearch.yaml b/integration_tests/k8s_resources/opensearch_crud/dev/someteamname/opensearch.yaml index a71e37b56..49bd42262 100644 --- a/integration_tests/k8s_resources/opensearch_crud/dev/someteamname/opensearch.yaml +++ b/integration_tests/k8s_resources/opensearch_crud/dev/someteamname/opensearch.yaml @@ -36,6 +36,12 @@ spec: terminationProtection: true userConfig: opensearch_version: "2" + opensearch: + http_max_content_length: 209715200 + indices_query_bool_max_clause_count: 512 + shard_indexing_pressure: + enabled: true + enforced: true status: conditions: - lastTransitionTime: "2023-11-08T10:36:06Z" diff --git a/integration_tests/k8s_resources/opensearch_crud/dev/someteamname/opensearch_noversion.yaml b/integration_tests/k8s_resources/opensearch_crud/dev/someteamname/opensearch_noversion.yaml index b1ceebab6..e0c455ecd 100644 --- a/integration_tests/k8s_resources/opensearch_crud/dev/someteamname/opensearch_noversion.yaml +++ b/integration_tests/k8s_resources/opensearch_crud/dev/someteamname/opensearch_noversion.yaml @@ -1,44 +1,12 @@ -apiVersion: aiven.io/v1alpha1 +apiVersion: nais.io/v1 kind: OpenSearch metadata: - annotations: - controllers.aiven.io/generation-was-processed: "2" - controllers.aiven.io/instance-is-running: "true" - nais.io/created_by: aiven-iac-migration - creationTimestamp: "2023-11-08T10:35:59Z" - finalizers: - - finalizers.aiven.io/delete-remote-resource - generation: 2 labels: - team: teampam nais.io/managed-by: console - name: opensearch-someteamname-noversion - namespace: teampam - resourceVersion: "3990043290" - uid: 2a8d4d8a-2bf4-4b2f-99fc-814f6a937ecd + name: noversion + namespace: someteamname spec: - cloudName: google-europe-north1 - connInfoSecretTarget: - name: "" - disk_space: 525G - plan: hobbyist - project: nav-prod - projectVpcId: fff21e17-95d5-408b-8df5-15aacf38f5de - tags: - environment: prod - team: teampam - tenant: nav - terminationProtection: true -status: - conditions: - - lastTransitionTime: "2023-11-08T10:36:06Z" - message: Instance was created or update on Aiven side - reason: Updated - status: "True" - type: Initialized - - lastTransitionTime: "2024-01-10T09:40:58Z" - message: Instance is running on Aiven side - reason: CheckRunning - status: "True" - type: Running - state: RUNNING + tier: SingleNode + memory: "2GB" + version: "2" + storageGB: 16 diff --git a/integration_tests/k8s_resources/valkey_crud/dev/someteamname/valkey.yaml b/integration_tests/k8s_resources/valkey_crud/dev/someteamname/valkey.yaml index e09c68366..fc196a689 100644 --- a/integration_tests/k8s_resources/valkey_crud/dev/someteamname/valkey.yaml +++ b/integration_tests/k8s_resources/valkey_crud/dev/someteamname/valkey.yaml @@ -13,6 +13,8 @@ spec: plan: startup-4 project: nav-dev projectVpcId: d405e36a-a577-4dce-af0e-6d217fc47a5c + userConfig: + valkey_persistence: "off" tags: environment: dev team: slug-1 diff --git a/integration_tests/opensearch_crud.lua b/integration_tests/opensearch_crud.lua index e54f2bb34..680fa53cb 100644 --- a/integration_tests/opensearch_crud.lua +++ b/integration_tests/opensearch_crud.lua @@ -137,7 +137,7 @@ Test.gql("Create opensearch as team member with existing name", function(t) errors = { { locations = NotNull(), - message = "Resource already exists.", + message = "OpenSearch with the name \"not-managed\" already exists, but are not yet managed through Console.", path = { "createOpenSearch", }, @@ -261,74 +261,144 @@ Test.gql("Create opensearch with invalid storage capacity increment", function(t } end) -Test.k8s("Validate OpenSearch resource", function(t) - local resourceName = string.format("opensearch-%s-foobar", mainTeam:slug()) +Test.gql("Create opensearch with out-of-range query bool max clause count", function(t) + t.addHeader("x-user-email", user:email()) + t.query [[ + mutation CreateOpenSearch { + createOpenSearch( + input: { + name: "foobar" + environmentName: "dev" + teamSlug: "someteamname" + tier: HIGH_AVAILABILITY + memory: GB_4 + version: V2 + storageGB: 240 + indicesQueryBoolMaxClauseCount: 8192 + } + ) { + openSearch { + name + } + } + } + ]] - t.check("aiven.io/v1alpha1", "opensearches", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", - kind = "OpenSearch", - metadata = { - name = resourceName, - namespace = mainTeam:slug(), - annotations = { - ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), - }, - labels = { - ["app.kubernetes.io/managed-by"] = "console", - ["nais.io/managed-by"] = "console", + t.check { + errors = { + { + extensions = { + field = "indicesQueryBoolMaxClauseCount", + }, + message = "Query bool max clause count must be between 64 and 4096.", + path = { + "createOpenSearch", + }, }, }, - spec = { - project = "aiven-dev", - projectVpcId = "aiven-vpc", - plan = "startup-16", - cloudName = "google-europe-north1", - disk_space = "350G", - terminationProtection = true, - tags = { - environment = "dev", - team = mainTeam:slug(), - tenant = "some-tenant", - }, - userConfig = { - opensearch_version = "2", + data = Null, + } +end) + +Test.gql("Create opensearch with invalid max content length", function(t) + t.addHeader("x-user-email", user:email()) + t.query [[ + mutation CreateOpenSearch { + createOpenSearch( + input: { + name: "foobar" + environmentName: "dev" + teamSlug: "someteamname" + tier: HIGH_AVAILABILITY + memory: GB_4 + version: V2 + storageGB: 240 + httpMaxContentLength: "not-a-quantity" + } + ) { + openSearch { + name + } + } + } + ]] + + t.check { + errors = { + { + extensions = { + field = "httpMaxContentLength", + }, + message = "Max content length must be a valid quantity (e.g. \"100Mi\", \"1Gi\").", + path = { + "createOpenSearch", + }, }, }, - }) + data = Null, + } end) -Test.k8s("Validate serviceintegration", function(t) - local resourceName = string.format("opensearch-%s-foobar", mainTeam:slug()) +Test.gql("Create opensearch with out-of-range max content length", function(t) + t.addHeader("x-user-email", user:email()) + t.query [[ + mutation CreateOpenSearch { + createOpenSearch( + input: { + name: "foobar" + environmentName: "dev" + teamSlug: "someteamname" + tier: HIGH_AVAILABILITY + memory: GB_4 + version: V2 + storageGB: 240 + httpMaxContentLength: "4Gi" + } + ) { + openSearch { + name + } + } + } + ]] - t.check("aiven.io/v1alpha1", "serviceintegrations", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", - kind = "ServiceIntegration", + t.check { + errors = { + { + extensions = { + field = "httpMaxContentLength", + }, + message = "Max content length must be between 1 byte and 2147483647 bytes (around 2047Mi).", + path = { + "createOpenSearch", + }, + }, + }, + data = Null, + } +end) + +Test.k8s("Validate OpenSearch resource", function(t) + t.check("nais.io/v1", "opensearches", "dev", mainTeam:slug(), "foobar", { + apiVersion = "nais.io/v1", + kind = "OpenSearch", metadata = { - name = resourceName, - namespace = mainTeam:slug(), annotations = { ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), + ["console.nais.io/last-modified-by"] = "user@usersen.com", }, labels = { ["app.kubernetes.io/managed-by"] = "console", ["nais.io/managed-by"] = "console", }, - ownerReferences = { - { - apiVersion = "aiven.io/v1alpha1", - kind = "OpenSearch", - name = resourceName, - uid = NotNull(), - }, - }, + name = "foobar", + namespace = "someteamname", }, spec = { - project = "aiven-dev", - destinationEndpointId = "endpoint-id", - integrationType = "prometheus", - sourceServiceName = resourceName, + memory = "16GB", + tier = "SingleNode", + version = "2", + storageGB = NotNull(), }, }) end) @@ -367,73 +437,26 @@ Test.gql("Create opensearch with tier and memory equivalent to hobbyist plan", f end) Test.k8s("Validate hobbyist OpenSearch resource", function(t) - local resourceName = string.format("opensearch-%s-foobar-hobbyist", mainTeam:slug()) - - t.check("aiven.io/v1alpha1", "opensearches", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", + t.check("nais.io/v1", "opensearches", "dev", mainTeam:slug(), "foobar-hobbyist", { + apiVersion = "nais.io/v1", kind = "OpenSearch", metadata = { - name = resourceName, - namespace = mainTeam:slug(), - annotations = { - ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), - }, - labels = { - ["app.kubernetes.io/managed-by"] = "console", - ["nais.io/managed-by"] = "console", - }, - }, - spec = { - project = "aiven-dev", - projectVpcId = "aiven-vpc", - plan = "hobbyist", - cloudName = "google-europe-north1", - disk_space = "16G", - terminationProtection = true, - tags = { - environment = "dev", - team = mainTeam:slug(), - tenant = "some-tenant", - }, - userConfig = { - opensearch_version = "2", - }, - }, - }) -end) - -Test.k8s("Validate hobbyist serviceintegration", function(t) - local resourceName = string.format("opensearch-%s-foobar-hobbyist", mainTeam:slug()) - - t.check("aiven.io/v1alpha1", "serviceintegrations", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", - kind = "ServiceIntegration", - metadata = { - name = resourceName, - namespace = mainTeam:slug(), annotations = { ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), + ["console.nais.io/last-modified-by"] = "user@usersen.com", }, labels = { ["app.kubernetes.io/managed-by"] = "console", ["nais.io/managed-by"] = "console", }, - ownerReferences = { - { - apiVersion = "aiven.io/v1alpha1", - kind = "OpenSearch", - name = resourceName, - uid = NotNull(), - }, - }, + name = "foobar-hobbyist", + namespace = "someteamname", }, spec = { - project = "aiven-dev", - destinationEndpointId = "endpoint-id", - integrationType = "prometheus", - sourceServiceName = resourceName, + memory = "2GB", + tier = "SingleNode", + version = "2", + storageGB = NotNull(), }, }) end) @@ -523,10 +546,18 @@ Test.gql("Update OpenSearch as team-member", function(t) memory: GB_4 version: V2 storageGB: 1020 + shardIndexingPressureEnabled: true + shardIndexingPressureEnforced: true + indicesQueryBoolMaxClauseCount: 2048 + httpMaxContentLength: "100Mi" } ) { openSearch { name + shardIndexingPressureEnabled + shardIndexingPressureEnforced + indicesQueryBoolMaxClauseCount + httpMaxContentLength } } } @@ -537,6 +568,10 @@ Test.gql("Update OpenSearch as team-member", function(t) updateOpenSearch = { openSearch = { name = "foobar", + shardIndexingPressureEnabled = true, + shardIndexingPressureEnforced = true, + indicesQueryBoolMaxClauseCount = 2048, + httpMaxContentLength = "100Mi", }, }, }, @@ -544,37 +579,36 @@ Test.gql("Update OpenSearch as team-member", function(t) end) Test.k8s("Validate OpenSearch resource after update", function(t) - local resourceName = string.format("opensearch-%s-foobar", mainTeam:slug()) - - t.check("aiven.io/v1alpha1", "opensearches", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", + t.check("nais.io/v1", "opensearches", "dev", mainTeam:slug(), "foobar", { + apiVersion = "nais.io/v1", kind = "OpenSearch", metadata = { - name = resourceName, - namespace = mainTeam:slug(), annotations = { ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), + ["console.nais.io/last-modified-by"] = "user@usersen.com", }, labels = { ["app.kubernetes.io/managed-by"] = "console", ["nais.io/managed-by"] = "console", }, + name = "foobar", + namespace = "someteamname", }, spec = { - project = "aiven-dev", - projectVpcId = "aiven-vpc", - plan = "business-4", - cloudName = "google-europe-north1", - disk_space = "1020G", - terminationProtection = true, - tags = { - environment = "dev", - team = mainTeam:slug(), - tenant = "some-tenant", + memory = "4GB", + tier = "HighAvailability", + version = "2", + storageGB = NotNull(), + shardIndexingPressure = { + enabled = true, + enforced = true, }, - userConfig = { - opensearch_version = "2", + indices = { + -- TODO: more precise assertion? + queryBoolMaxClauseCount = NotNull(), + }, + http = { + maxContentLength = "100Mi", }, }, }) @@ -591,6 +625,10 @@ Test.gql("List opensearches for team", function(t) name tier memory + shardIndexingPressureEnabled + shardIndexingPressureEnforced + indicesQueryBoolMaxClauseCount + httpMaxContentLength } } } @@ -606,26 +644,46 @@ Test.gql("List opensearches for team", function(t) name = "foobar", tier = "HIGH_AVAILABILITY", memory = "GB_4", + shardIndexingPressureEnabled = true, + shardIndexingPressureEnforced = true, + indicesQueryBoolMaxClauseCount = 2048, + httpMaxContentLength = "100Mi", }, { name = "foobar-hobbyist", tier = "SINGLE_NODE", memory = "GB_2", + shardIndexingPressureEnabled = false, + shardIndexingPressureEnforced = false, + indicesQueryBoolMaxClauseCount = Null, + httpMaxContentLength = Null, }, { name = "noversion", tier = "SINGLE_NODE", memory = "GB_2", + shardIndexingPressureEnabled = false, + shardIndexingPressureEnforced = false, + indicesQueryBoolMaxClauseCount = Null, + httpMaxContentLength = Null, }, { name = "opensearch-someteamname-hobbyist-not-managed", tier = "SINGLE_NODE", memory = "GB_2", + shardIndexingPressureEnabled = false, + shardIndexingPressureEnforced = false, + indicesQueryBoolMaxClauseCount = Null, + httpMaxContentLength = Null, }, { name = "opensearch-someteamname-not-managed", tier = "HIGH_AVAILABILITY", memory = "GB_8", + shardIndexingPressureEnabled = true, + shardIndexingPressureEnforced = true, + indicesQueryBoolMaxClauseCount = 512, + httpMaxContentLength = "200Mi", }, }, }, @@ -670,7 +728,7 @@ Test.gql("Downgrade OpenSearch as team-member", function(t) } end) -Test.gql("Downgrade OpenSearch without explicit version set", function(t) +Test.gql("Downgrade OpenSearch noversion instance", function(t) t.addHeader("x-user-email", user:email()) t.query [[ mutation UpdateOpenSearch { @@ -706,42 +764,6 @@ Test.gql("Downgrade OpenSearch without explicit version set", function(t) } end) -Test.gql("Update non-console managed OpenSearch as team-member", function(t) - t.addHeader("x-user-email", user:email()) - t.query [[ - mutation UpdateOpenSearch { - updateOpenSearch( - input: { - name: "not-managed" - environmentName: "dev" - teamSlug: "someteamname" - tier: HIGH_AVAILABILITY - memory: GB_4 - version: V2 - storageGB: 240 - } - ) { - openSearch { - name - } - } - } - ]] - - t.check { - errors = { - { - locations = NotNull(), - message = "OpenSearch someteamname/not-managed is not managed by Console", - path = { - "updateOpenSearch", - }, - }, - }, - data = Null, - } -end) - Test.gql("Update OpenSearch with tier and memory equivalent to hobbyist plan", function(t) t.addHeader("x-user-email", user:email()) t.query [[ @@ -776,37 +798,36 @@ Test.gql("Update OpenSearch with tier and memory equivalent to hobbyist plan", f end) Test.k8s("Validate hobbyist OpenSearch resource after update", function(t) - local resourceName = string.format("opensearch-%s-foobar", mainTeam:slug()) - - t.check("aiven.io/v1alpha1", "opensearches", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", + t.check("nais.io/v1", "opensearches", "dev", mainTeam:slug(), "foobar", { + apiVersion = "nais.io/v1", kind = "OpenSearch", metadata = { - name = resourceName, - namespace = mainTeam:slug(), annotations = { ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), + ["console.nais.io/last-modified-by"] = "user@usersen.com", }, labels = { ["app.kubernetes.io/managed-by"] = "console", ["nais.io/managed-by"] = "console", }, + name = "foobar", + namespace = "someteamname", }, spec = { - project = "aiven-dev", - projectVpcId = "aiven-vpc", - plan = "hobbyist", - cloudName = "google-europe-north1", - disk_space = "16G", - terminationProtection = true, - tags = { - environment = "dev", - team = mainTeam:slug(), - tenant = "some-tenant", + memory = "2GB", + tier = "SingleNode", + version = "2", + storageGB = NotNull(), + shardIndexingPressure = { + enabled = true, + enforced = true, + }, + indices = { + -- TODO: more precise assertion? + queryBoolMaxClauseCount = NotNull(), }, - userConfig = { - opensearch_version = "2", + http = { + maxContentLength = "100Mi", }, }, }) @@ -1077,6 +1098,26 @@ Test.gql("Verify activity log for opensearch operations", function(t) oldValue = "350", newValue = "1020", }, + { + field = "shardIndexingPressureEnabled", + oldValue = "false", + newValue = "true", + }, + { + field = "shardIndexingPressureEnforced", + oldValue = "false", + newValue = "true", + }, + { + field = "indicesQueryBoolMaxClauseCount", + oldValue = Null, + newValue = "2048", + }, + { + field = "httpMaxContentLength", + oldValue = Null, + newValue = "100Mi", + }, }, }, }, @@ -1106,33 +1147,3 @@ Test.gql("Verify activity log for opensearch operations", function(t) }, } end) - -Test.gql("Delete non-managed opensearch as team-member", function(t) - t.addHeader("x-user-email", user:email()) - t.query [[ - mutation DeleteOpenSearch { - deleteOpenSearch( - input: { - name: "not-managed" - environmentName: "dev" - teamSlug: "someteamname" - } - ) { - openSearchDeleted - } - } - ]] - - t.check { - errors = { - { - locations = NotNull(), - message = "OpenSearch someteamname/not-managed is not managed by Console", - path = { - "deleteOpenSearch", - }, - }, - }, - data = Null, - } -end) diff --git a/integration_tests/valkey_crud.lua b/integration_tests/valkey_crud.lua index 4f9aabc80..658fbcd69 100644 --- a/integration_tests/valkey_crud.lua +++ b/integration_tests/valkey_crud.lua @@ -86,12 +86,10 @@ Test.gql("Create valkey as team member", function(t) teamSlug: "someteamname" tier: SINGLE_NODE memory: GB_14 - databases: 32 } ) { valkey { name - databases } } } @@ -102,7 +100,6 @@ Test.gql("Create valkey as team member", function(t) createValkey = { valkey = { name = "foobar", - databases = 32, }, }, }, @@ -133,7 +130,7 @@ Test.gql("Create valkey as team member with existing name", function(t) errors = { { locations = NotNull(), - message = "Resource already exists.", + message = "Valkey with the name \"not-managed\" already exists, but are not yet managed through Console.", path = { "createValkey", }, @@ -218,72 +215,24 @@ Test.gql("Create valkey with databases above maximum", function(t) end) Test.k8s("Validate Valkey resource", function(t) - local resourceName = string.format("valkey-%s-foobar", mainTeam:slug()) - - t.check("aiven.io/v1alpha1", "valkeys", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", + t.check("nais.io/v1", "valkeys", "dev", mainTeam:slug(), "foobar", { + apiVersion = "nais.io/v1", kind = "Valkey", metadata = { - name = resourceName, - namespace = mainTeam:slug(), - annotations = { - ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), - }, - labels = { - ["app.kubernetes.io/managed-by"] = "console", - ["nais.io/managed-by"] = "console", - }, - }, - spec = { - project = "aiven-dev", - projectVpcId = "aiven-vpc", - plan = "startup-14", - cloudName = "google-europe-north1", - terminationProtection = true, - userConfig = { - valkey_number_of_databases = 32, - }, - tags = { - environment = "dev", - team = mainTeam:slug(), - tenant = "some-tenant", - }, - }, - }) -end) - -Test.k8s("Validate serviceintegration", function(t) - local resourceName = string.format("valkey-%s-foobar", mainTeam:slug()) - - t.check("aiven.io/v1alpha1", "serviceintegrations", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", - kind = "ServiceIntegration", - metadata = { - name = resourceName, - namespace = mainTeam:slug(), annotations = { ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), + ["console.nais.io/last-modified-by"] = "user@usersen.com", }, labels = { ["app.kubernetes.io/managed-by"] = "console", ["nais.io/managed-by"] = "console", }, - ownerReferences = { - { - apiVersion = "aiven.io/v1alpha1", - kind = "Valkey", - name = resourceName, - uid = NotNull(), - }, - }, + name = "foobar", + namespace = "someteamname", }, spec = { - project = "aiven-dev", - destinationEndpointId = "endpoint-id", - integrationType = "prometheus", - sourceServiceName = resourceName, + memory = "14GB", + tier = "SingleNode", }, }) end) @@ -370,11 +319,13 @@ Test.gql("Update Valkey as team-member", function(t) maxMemoryPolicy: ALLKEYS_RANDOM notifyKeyspaceEvents: "Exd" databases: 64 + persistenceDisabled: true } ) { valkey { name databases + persistenceDisabled } } } @@ -386,6 +337,7 @@ Test.gql("Update Valkey as team-member", function(t) valkey = { name = "foobar", databases = 64, + persistenceDisabled = true, }, }, }, @@ -393,39 +345,30 @@ Test.gql("Update Valkey as team-member", function(t) end) Test.k8s("Validate Valkey resource after update", function(t) - local resourceName = string.format("valkey-%s-foobar", mainTeam:slug()) - - t.check("aiven.io/v1alpha1", "valkeys", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", + t.check("nais.io/v1", "valkeys", "dev", mainTeam:slug(), "foobar", { + apiVersion = "nais.io/v1", kind = "Valkey", metadata = { - name = resourceName, - namespace = mainTeam:slug(), annotations = { ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), + ["console.nais.io/last-modified-by"] = "user@usersen.com", }, labels = { ["app.kubernetes.io/managed-by"] = "console", ["nais.io/managed-by"] = "console", }, + name = "foobar", + namespace = "someteamname", }, spec = { - project = "aiven-dev", - projectVpcId = "aiven-vpc", - plan = "business-4", - cloudName = "google-europe-north1", - terminationProtection = true, - userConfig = { - valkey_maxmemory_policy = "allkeys-random", - valkey_notify_keyspace_events = "Exd", - valkey_number_of_databases = 64, - }, - tags = { - environment = "dev", - team = mainTeam:slug(), - tenant = "some-tenant", + databases = 64, + maxMemoryPolicy = "allkeys-random", + memory = "4GB", + notifyKeyspaceEvents = "Exd", + persistence = { + disabled = true, }, + tier = "HighAvailability", }, }) end) @@ -461,70 +404,26 @@ Test.gql("Create valkey with tier and memory equivalent to hobbyist plan", funct } end) +-- TODO: Do we need this? Test.k8s("Validate hobbyist Valkey resource", function(t) - local resourceName = string.format("valkey-%s-foobar-hobbyist", mainTeam:slug()) - - t.check("aiven.io/v1alpha1", "valkeys", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", + t.check("nais.io/v1", "valkeys", "dev", mainTeam:slug(), "foobar-hobbyist", { + apiVersion = "nais.io/v1", kind = "Valkey", metadata = { - name = resourceName, - namespace = mainTeam:slug(), annotations = { ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), + ["console.nais.io/last-modified-by"] = "user@usersen.com", }, labels = { ["app.kubernetes.io/managed-by"] = "console", ["nais.io/managed-by"] = "console", }, + name = "foobar-hobbyist", + namespace = "someteamname", }, spec = { - project = "aiven-dev", - projectVpcId = "aiven-vpc", - plan = "hobbyist", - cloudName = "google-europe-north1", - terminationProtection = true, - tags = { - environment = "dev", - team = mainTeam:slug(), - tenant = "some-tenant", - }, - }, - }) -end) - -Test.k8s("Validate hobbyist serviceintegration", function(t) - local resourceName = string.format("valkey-%s-foobar-hobbyist", mainTeam:slug()) - - t.check("aiven.io/v1alpha1", "serviceintegrations", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", - kind = "ServiceIntegration", - metadata = { - name = resourceName, - namespace = mainTeam:slug(), - annotations = { - ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), - }, - labels = { - ["app.kubernetes.io/managed-by"] = "console", - ["nais.io/managed-by"] = "console", - }, - ownerReferences = { - { - apiVersion = "aiven.io/v1alpha1", - kind = "Valkey", - name = resourceName, - uid = NotNull(), - }, - }, - }, - spec = { - project = "aiven-dev", - destinationEndpointId = "endpoint-id", - integrationType = "prometheus", - sourceServiceName = resourceName, + memory = "1GB", + tier = "SingleNode", }, }) end) @@ -543,6 +442,7 @@ Test.gql("List valkeys for team", function(t) maxMemoryPolicy notifyKeyspaceEvents databases + persistenceDisabled } } } @@ -561,6 +461,7 @@ Test.gql("List valkeys for team", function(t) maxMemoryPolicy = "ALLKEYS_RANDOM", notifyKeyspaceEvents = "Exd", databases = 64, + persistenceDisabled = true, }, { name = "foobar-hobbyist", @@ -569,6 +470,7 @@ Test.gql("List valkeys for team", function(t) maxMemoryPolicy = "", notifyKeyspaceEvents = "", databases = 16, + persistenceDisabled = false, }, { name = "valkey-someteamname-hobbyist-not-managed", @@ -577,6 +479,7 @@ Test.gql("List valkeys for team", function(t) maxMemoryPolicy = "", notifyKeyspaceEvents = "", databases = 16, + persistenceDisabled = false, }, { name = "valkey-someteamname-not-managed", @@ -585,6 +488,7 @@ Test.gql("List valkeys for team", function(t) maxMemoryPolicy = "", notifyKeyspaceEvents = "", databases = 16, + persistenceDisabled = true, }, }, }, @@ -593,42 +497,6 @@ Test.gql("List valkeys for team", function(t) } end) -Test.gql("Update non-console managed Valkey as team-member", function(t) - t.addHeader("x-user-email", user:email()) - t.query [[ - mutation UpdateValkey { - updateValkey( - input: { - name: "not-managed" - environmentName: "dev" - teamSlug: "someteamname" - tier: HIGH_AVAILABILITY - memory: GB_4 - maxMemoryPolicy: ALLKEYS_RANDOM - notifyKeyspaceEvents: "Exd" - } - ) { - valkey { - name - } - } - } - ]] - - t.check { - errors = { - { - locations = NotNull(), - message = "Valkey someteamname/not-managed is not managed by Console", - path = { - "updateValkey", - }, - }, - }, - data = Null, - } -end) - Test.gql("Update Valkey with tier and memory equivalent to hobbyist plan", function(t) t.addHeader("x-user-email", user:email()) t.query [[ @@ -663,39 +531,30 @@ Test.gql("Update Valkey with tier and memory equivalent to hobbyist plan", funct end) Test.k8s("Validate hobbyist Valkey resource after update", function(t) - local resourceName = string.format("valkey-%s-foobar", mainTeam:slug()) - - t.check("aiven.io/v1alpha1", "valkeys", "dev", mainTeam:slug(), resourceName, { - apiVersion = "aiven.io/v1alpha1", + t.check("nais.io/v1", "valkeys", "dev", mainTeam:slug(), "foobar", { + apiVersion = "nais.io/v1", kind = "Valkey", metadata = { - name = resourceName, - namespace = mainTeam:slug(), annotations = { ["console.nais.io/last-modified-at"] = NotNull(), - ["console.nais.io/last-modified-by"] = user:email(), + ["console.nais.io/last-modified-by"] = "user@usersen.com", }, labels = { ["app.kubernetes.io/managed-by"] = "console", ["nais.io/managed-by"] = "console", }, + name = "foobar", + namespace = "someteamname", }, spec = { - project = "aiven-dev", - projectVpcId = "aiven-vpc", - plan = "hobbyist", - cloudName = "google-europe-north1", - terminationProtection = true, - userConfig = { - valkey_maxmemory_policy = "allkeys-random", - valkey_notify_keyspace_events = "Exd", - valkey_number_of_databases = 64, - }, - tags = { - environment = "dev", - team = mainTeam:slug(), - tenant = "some-tenant", + databases = 64, + maxMemoryPolicy = "allkeys-random", + memory = "1GB", + notifyKeyspaceEvents = "Exd", + persistence = { + disabled = true, }, + tier = "SingleNode", }, }) end) @@ -830,36 +689,6 @@ Test.gql("Verify activity log after deleting valkey", function(t) } end) -Test.gql("Delete non-managed valkey as team-member", function(t) - t.addHeader("x-user-email", user:email()) - t.query [[ - mutation DeleteValkey { - deleteValkey( - input: { - name: "not-managed" - environmentName: "dev" - teamSlug: "someteamname" - } - ) { - valkeyDeleted - } - } - ]] - - t.check { - errors = { - { - locations = NotNull(), - message = "Valkey someteamname/not-managed is not managed by Console", - path = { - "deleteValkey", - }, - }, - }, - data = Null, - } -end) - Test.gql("Verify activity log for valkey operations", function(t) t.addHeader("x-user-email", user:email()) t.query(string.format([[ @@ -972,9 +801,14 @@ Test.gql("Verify activity log for valkey operations", function(t) }, { field = "databases", - oldValue = "32", + oldValue = Null, newValue = "64", }, + { + field = "persistenceDisabled", + oldValue = "false", + newValue = "true", + }, }, }, }, diff --git a/internal/cmd/api/http.go b/internal/cmd/api/http.go index c0d939824..f2f299e60 100644 --- a/internal/cmd/api/http.go +++ b/internal/cmd/api/http.go @@ -348,8 +348,8 @@ func ConfigureGraph( ctx = config.NewLoaderContext(ctx, watchers.ConfigWatcher, log) ctx = instancegroup.NewLoaderContext(ctx, watchers.ReplicaSetWatcher, watchers.PodWatcher, watchers.AppWatcher, dynamicClients, log) ctx = aiven.NewLoaderContext(ctx, aivenProjects) - ctx = opensearch.NewLoaderContext(ctx, tenantName, watchers.OpenSearchWatcher, aivenClient, log) - ctx = valkey.NewLoaderContext(ctx, tenantName, watchers.ValkeyWatcher, aivenClient) + ctx = opensearch.NewLoaderContext(ctx, tenantName, watchers.OpenSearchWatcher, watchers.NaisOpenSearchWatcher, aivenClient, log) + ctx = valkey.NewLoaderContext(ctx, tenantName, watchers.ValkeyWatcher, watchers.NaisValkeyWatcher, aivenClient) ctx = price.NewLoaderContext(ctx, priceRetriever, log) ctx = utilization.NewLoaderContext(ctx, prometheusClient, log) ctx = alerts.NewLoaderContext(ctx, prometheusClient, log) diff --git a/internal/graph/apierror/apierror.go b/internal/graph/apierror/apierror.go index 181fd663b..20ccca0f6 100644 --- a/internal/graph/apierror/apierror.go +++ b/internal/graph/apierror/apierror.go @@ -64,6 +64,8 @@ func GetErrorPresenter(log logrus.FieldLogger) graphql.ErrorPresenterFunc { case k8serrors.IsNotFound(unwrappedError): unwrappedError = ErrNotFound case k8serrors.IsForbidden(unwrappedError): + log.WithError(unwrappedError).Errorf("kubernetes api permission error") + unwrappedError = ErrForbidden } diff --git a/internal/graph/gengql/opensearch.generated.go b/internal/graph/gengql/opensearch.generated.go index 4f678dcfa..f1f34008b 100644 --- a/internal/graph/gengql/opensearch.generated.go +++ b/internal/graph/gengql/opensearch.generated.go @@ -659,6 +659,98 @@ func (ec *executionContext) fieldContext_OpenSearch_storageGB(_ context.Context, return graphql.NewScalarFieldContext("OpenSearch", field, false, false, errors.New("field of type Int does not have child fields")) } +func (ec *executionContext) _OpenSearch_shardIndexingPressureEnabled(ctx context.Context, field graphql.CollectedField, obj *opensearch.OpenSearch) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_OpenSearch_shardIndexingPressureEnabled(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ShardIndexingPressureEnabled, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_OpenSearch_shardIndexingPressureEnabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("OpenSearch", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _OpenSearch_shardIndexingPressureEnforced(ctx context.Context, field graphql.CollectedField, obj *opensearch.OpenSearch) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_OpenSearch_shardIndexingPressureEnforced(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.ShardIndexingPressureEnforced, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_OpenSearch_shardIndexingPressureEnforced(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("OpenSearch", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + +func (ec *executionContext) _OpenSearch_indicesQueryBoolMaxClauseCount(ctx context.Context, field graphql.CollectedField, obj *opensearch.OpenSearch) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_OpenSearch_indicesQueryBoolMaxClauseCount(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.IndicesQueryBoolMaxClauseCount, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *int) graphql.Marshaler { + return ec.marshalOInt2ᚖint(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_OpenSearch_indicesQueryBoolMaxClauseCount(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("OpenSearch", field, false, false, errors.New("field of type Int does not have child fields")) +} + +func (ec *executionContext) _OpenSearch_httpMaxContentLength(ctx context.Context, field graphql.CollectedField, obj *opensearch.OpenSearch) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_OpenSearch_httpMaxContentLength(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.HTTPMaxContentLength, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v *string) graphql.Marshaler { + return ec.marshalOString2ᚖstring(ctx, selections, v) + }, + true, + false, + ) +} +func (ec *executionContext) fieldContext_OpenSearch_httpMaxContentLength(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("OpenSearch", field, false, false, errors.New("field of type String does not have child fields")) +} + func (ec *executionContext) _OpenSearch_issues(ctx context.Context, field graphql.CollectedField, obj *opensearch.OpenSearch) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -2284,7 +2376,7 @@ func (ec *executionContext) unmarshalInputCreateOpenSearchInput(ctx context.Cont asMap[k] = v } - fieldsInOrder := [...]string{"name", "environmentName", "teamSlug", "tier", "memory", "version", "storageGB"} + fieldsInOrder := [...]string{"name", "environmentName", "teamSlug", "tier", "memory", "version", "storageGB", "shardIndexingPressureEnabled", "shardIndexingPressureEnforced", "indicesQueryBoolMaxClauseCount", "httpMaxContentLength"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -2340,6 +2432,34 @@ func (ec *executionContext) unmarshalInputCreateOpenSearchInput(ctx context.Cont return it, err } it.StorageGB = data + case "shardIndexingPressureEnabled": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("shardIndexingPressureEnabled")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.ShardIndexingPressureEnabled = data + case "shardIndexingPressureEnforced": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("shardIndexingPressureEnforced")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.ShardIndexingPressureEnforced = data + case "indicesQueryBoolMaxClauseCount": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("indicesQueryBoolMaxClauseCount")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.IndicesQueryBoolMaxClauseCount = data + case "httpMaxContentLength": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("httpMaxContentLength")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.HTTPMaxContentLength = data } } return it, nil @@ -2518,7 +2638,7 @@ func (ec *executionContext) unmarshalInputUpdateOpenSearchInput(ctx context.Cont asMap[k] = v } - fieldsInOrder := [...]string{"name", "environmentName", "teamSlug", "tier", "memory", "version", "storageGB"} + fieldsInOrder := [...]string{"name", "environmentName", "teamSlug", "tier", "memory", "version", "storageGB", "shardIndexingPressureEnabled", "shardIndexingPressureEnforced", "indicesQueryBoolMaxClauseCount", "httpMaxContentLength"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -2574,6 +2694,34 @@ func (ec *executionContext) unmarshalInputUpdateOpenSearchInput(ctx context.Cont return it, err } it.StorageGB = data + case "shardIndexingPressureEnabled": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("shardIndexingPressureEnabled")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.ShardIndexingPressureEnabled = data + case "shardIndexingPressureEnforced": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("shardIndexingPressureEnforced")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.ShardIndexingPressureEnforced = data + case "indicesQueryBoolMaxClauseCount": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("indicesQueryBoolMaxClauseCount")) + data, err := ec.unmarshalOInt2ᚖint(ctx, v) + if err != nil { + return it, err + } + it.IndicesQueryBoolMaxClauseCount = data + case "httpMaxContentLength": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("httpMaxContentLength")) + data, err := ec.unmarshalOString2ᚖstring(ctx, v) + if err != nil { + return it, err + } + it.HTTPMaxContentLength = data } } return it, nil @@ -2991,6 +3139,20 @@ func (ec *executionContext) _OpenSearch(ctx context.Context, sel ast.SelectionSe if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "shardIndexingPressureEnabled": + out.Values[i] = ec._OpenSearch_shardIndexingPressureEnabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "shardIndexingPressureEnforced": + out.Values[i] = ec._OpenSearch_shardIndexingPressureEnforced(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } + case "indicesQueryBoolMaxClauseCount": + out.Values[i] = ec._OpenSearch_indicesQueryBoolMaxClauseCount(ctx, field, obj) + case "httpMaxContentLength": + out.Values[i] = ec._OpenSearch_httpMaxContentLength(ctx, field, obj) case "issues": field := field diff --git a/internal/graph/gengql/root_.generated.go b/internal/graph/gengql/root_.generated.go index 3eb956c72..857d669ee 100644 --- a/internal/graph/gengql/root_.generated.go +++ b/internal/graph/gengql/root_.generated.go @@ -1562,23 +1562,27 @@ type ComplexityRoot struct { } OpenSearch struct { - Access func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *opensearch.OpenSearchAccessOrder) int - ActivityLog func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, filter *activitylog.ActivityLogFilter) int - Cost func(childComplexity int) int - Environment func(childComplexity int) int - ID func(childComplexity int) int - Issues func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) int - Maintenance func(childComplexity int) int - Memory func(childComplexity int) int - Name func(childComplexity int) int - State func(childComplexity int) int - StorageGB func(childComplexity int) int - Team func(childComplexity int) int - TeamEnvironment func(childComplexity int) int - TerminationProtection func(childComplexity int) int - Tier func(childComplexity int) int - Version func(childComplexity int) int - Workload func(childComplexity int) int + Access func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *opensearch.OpenSearchAccessOrder) int + ActivityLog func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, filter *activitylog.ActivityLogFilter) int + Cost func(childComplexity int) int + Environment func(childComplexity int) int + HTTPMaxContentLength func(childComplexity int) int + ID func(childComplexity int) int + IndicesQueryBoolMaxClauseCount func(childComplexity int) int + Issues func(childComplexity int, first *int, after *pagination.Cursor, last *int, before *pagination.Cursor, orderBy *issue.IssueOrder, filter *issue.ResourceIssueFilter) int + Maintenance func(childComplexity int) int + Memory func(childComplexity int) int + Name func(childComplexity int) int + ShardIndexingPressureEnabled func(childComplexity int) int + ShardIndexingPressureEnforced func(childComplexity int) int + State func(childComplexity int) int + StorageGB func(childComplexity int) int + Team func(childComplexity int) int + TeamEnvironment func(childComplexity int) int + TerminationProtection func(childComplexity int) int + Tier func(childComplexity int) int + Version func(childComplexity int) int + Workload func(childComplexity int) int } OpenSearchAccess struct { @@ -3325,6 +3329,7 @@ type ComplexityRoot struct { Memory func(childComplexity int) int Name func(childComplexity int) int NotifyKeyspaceEvents func(childComplexity int) int + PersistenceDisabled func(childComplexity int) int State func(childComplexity int) int Team func(childComplexity int) int TeamEnvironment func(childComplexity int) int @@ -9773,6 +9778,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.OpenSearch.Environment(childComplexity), true + case "OpenSearch.httpMaxContentLength": + if e.ComplexityRoot.OpenSearch.HTTPMaxContentLength == nil { + break + } + + return e.ComplexityRoot.OpenSearch.HTTPMaxContentLength(childComplexity), true + case "OpenSearch.id": if e.ComplexityRoot.OpenSearch.ID == nil { break @@ -9780,6 +9792,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.OpenSearch.ID(childComplexity), true + case "OpenSearch.indicesQueryBoolMaxClauseCount": + if e.ComplexityRoot.OpenSearch.IndicesQueryBoolMaxClauseCount == nil { + break + } + + return e.ComplexityRoot.OpenSearch.IndicesQueryBoolMaxClauseCount(childComplexity), true + case "OpenSearch.issues": if e.ComplexityRoot.OpenSearch.Issues == nil { break @@ -9813,6 +9832,20 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.OpenSearch.Name(childComplexity), true + case "OpenSearch.shardIndexingPressureEnabled": + if e.ComplexityRoot.OpenSearch.ShardIndexingPressureEnabled == nil { + break + } + + return e.ComplexityRoot.OpenSearch.ShardIndexingPressureEnabled(childComplexity), true + + case "OpenSearch.shardIndexingPressureEnforced": + if e.ComplexityRoot.OpenSearch.ShardIndexingPressureEnforced == nil { + break + } + + return e.ComplexityRoot.OpenSearch.ShardIndexingPressureEnforced(childComplexity), true + case "OpenSearch.state": if e.ComplexityRoot.OpenSearch.State == nil { break @@ -17555,6 +17588,13 @@ func (e *executableSchema) Complexity(ctx context.Context, typeName, field strin return e.ComplexityRoot.Valkey.NotifyKeyspaceEvents(childComplexity), true + case "Valkey.persistenceDisabled": + if e.ComplexityRoot.Valkey.PersistenceDisabled == nil { + break + } + + return e.ComplexityRoot.Valkey.PersistenceDisabled(childComplexity), true + case "Valkey.state": if e.ComplexityRoot.Valkey.State == nil { break @@ -24208,6 +24248,14 @@ type OpenSearch implements Persistence & Node { memory: OpenSearchMemory! "Available storage in GB." storageGB: Int! + "Whether shard indexing back pressure is enabled." + shardIndexingPressureEnabled: Boolean! + "Whether shard indexing back pressure runs in enforced mode. In enforced mode requests that may degrade cluster performance are rejected; in shadow mode (enforced false) metrics are tracked but no requests are rejected." + shardIndexingPressureEnforced: Boolean! + "Maximum number of clauses a Lucene BooleanQuery can contain. When not set, the instance uses the default of 1024. Increasing this value may cause performance issues." + indicesQueryBoolMaxClauseCount: Int + "Maximum content length, in a human-readable quantity (e.g. \"100Mi\", \"1Gi\"), for requests to the OpenSearch HTTP API. When not set, the instance uses the default of 100Mi." + httpMaxContentLength: String "Issues that affects the instance." issues( "Get the first n items in the connection. This can be used in combination with the after parameter." @@ -24374,6 +24422,14 @@ input CreateOpenSearchInput { version: OpenSearchMajorVersion! "Available storage in GB." storageGB: Int! + "Enable shard indexing back pressure. Defaults to false." + shardIndexingPressureEnabled: Boolean + "Run shard indexing back pressure in enforced mode. Defaults to false (shadow mode)." + shardIndexingPressureEnforced: Boolean + "Maximum number of clauses a Lucene BooleanQuery can contain. Must be between 64 and 4096. When not set, the instance uses the default of 1024." + indicesQueryBoolMaxClauseCount: Int + "Maximum content length for requests to the OpenSearch HTTP API. Specified as a human-readable quantity (e.g. \"100Mi\", \"1Gi\"); unitless values are interpreted as bytes. Must be between 1 byte and 2147483647 bytes (around 2047Mi). When not set, the instance uses the default of 100Mi." + httpMaxContentLength: String } type CreateOpenSearchPayload { @@ -24396,6 +24452,14 @@ input UpdateOpenSearchInput { version: OpenSearchMajorVersion! "Available storage in GB." storageGB: Int! + "Enable shard indexing back pressure. Defaults to false." + shardIndexingPressureEnabled: Boolean + "Run shard indexing back pressure in enforced mode. Defaults to false (shadow mode)." + shardIndexingPressureEnforced: Boolean + "Maximum number of clauses a Lucene BooleanQuery can contain. Must be between 64 and 4096. When not set, the instance uses the default of 1024." + indicesQueryBoolMaxClauseCount: Int + "Maximum content length for requests to the OpenSearch HTTP API. Specified as a human-readable quantity (e.g. \"100Mi\", \"1Gi\"); unitless values are interpreted as bytes. Must be between 1 byte and 2147483647 bytes (around 2047Mi). When not set, the instance uses the default of 100Mi." + httpMaxContentLength: String } type UpdateOpenSearchPayload { @@ -30138,6 +30202,8 @@ type Valkey implements Persistence & Node { notifyKeyspaceEvents: String "Number of databases the Valkey instance is configured with. Default is 16. Minimum 1, maximum 128. Changing this will cause a restart of the Valkey service." databases: Int! + "Whether persistence (RDB dumps and backups) is disabled. If true, all data is lost if the instance is restarted for any reason." + persistenceDisabled: Boolean! "Issues that affects the instance." issues( "Get the first n items in the connection. This can be used in combination with the after parameter." @@ -30323,6 +30389,8 @@ input CreateValkeyInput { notifyKeyspaceEvents: String "Number of databases. Default is 16. Minimum 1, maximum 128. Changing this will cause a restart of the Valkey service." databases: Int + "Disable persistence (RDB dumps and backups). Defaults to false." + persistenceDisabled: Boolean } type CreateValkeyPayload { @@ -30347,6 +30415,8 @@ input UpdateValkeyInput { notifyKeyspaceEvents: String "Number of databases. Default is 16. Minimum 1, maximum 128. Changing this will cause a restart of the Valkey service." databases: Int + "Disable persistence (RDB dumps and backups). Defaults to false." + persistenceDisabled: Boolean } type UpdateValkeyPayload { @@ -33850,6 +33920,14 @@ func (ec *executionContext) childFields_OpenSearch(ctx context.Context, field gr return ec.fieldContext_OpenSearch_memory(ctx, field) case "storageGB": return ec.fieldContext_OpenSearch_storageGB(ctx, field) + case "shardIndexingPressureEnabled": + return ec.fieldContext_OpenSearch_shardIndexingPressureEnabled(ctx, field) + case "shardIndexingPressureEnforced": + return ec.fieldContext_OpenSearch_shardIndexingPressureEnforced(ctx, field) + case "indicesQueryBoolMaxClauseCount": + return ec.fieldContext_OpenSearch_indicesQueryBoolMaxClauseCount(ctx, field) + case "httpMaxContentLength": + return ec.fieldContext_OpenSearch_httpMaxContentLength(ctx, field) case "issues": return ec.fieldContext_OpenSearch_issues(ctx, field) case "activityLog": @@ -36136,6 +36214,8 @@ func (ec *executionContext) childFields_Valkey(ctx context.Context, field graphq return ec.fieldContext_Valkey_notifyKeyspaceEvents(ctx, field) case "databases": return ec.fieldContext_Valkey_databases(ctx, field) + case "persistenceDisabled": + return ec.fieldContext_Valkey_persistenceDisabled(ctx, field) case "issues": return ec.fieldContext_Valkey_issues(ctx, field) case "activityLog": diff --git a/internal/graph/gengql/valkey.generated.go b/internal/graph/gengql/valkey.generated.go index 5987a90fc..e0116101e 100644 --- a/internal/graph/gengql/valkey.generated.go +++ b/internal/graph/gengql/valkey.generated.go @@ -726,6 +726,29 @@ func (ec *executionContext) fieldContext_Valkey_databases(_ context.Context, fie return graphql.NewScalarFieldContext("Valkey", field, false, false, errors.New("field of type Int does not have child fields")) } +func (ec *executionContext) _Valkey_persistenceDisabled(ctx context.Context, field graphql.CollectedField, obj *valkey.Valkey) (ret graphql.Marshaler) { + return graphql.ResolveField( + ctx, + ec.OperationContext, + field, + func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) { + return ec.fieldContext_Valkey_persistenceDisabled(ctx, field) + }, + func(ctx context.Context) (any, error) { + return obj.PersistenceDisabled, nil + }, + nil, + func(ctx context.Context, selections ast.SelectionSet, v bool) graphql.Marshaler { + return ec.marshalNBoolean2bool(ctx, selections, v) + }, + true, + true, + ) +} +func (ec *executionContext) fieldContext_Valkey_persistenceDisabled(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) { + return graphql.NewScalarFieldContext("Valkey", field, false, false, errors.New("field of type Boolean does not have child fields")) +} + func (ec *executionContext) _Valkey_issues(ctx context.Context, field graphql.CollectedField, obj *valkey.Valkey) (ret graphql.Marshaler) { return graphql.ResolveField( ctx, @@ -2250,7 +2273,7 @@ func (ec *executionContext) unmarshalInputCreateValkeyInput(ctx context.Context, asMap[k] = v } - fieldsInOrder := [...]string{"name", "environmentName", "teamSlug", "tier", "memory", "maxMemoryPolicy", "notifyKeyspaceEvents", "databases"} + fieldsInOrder := [...]string{"name", "environmentName", "teamSlug", "tier", "memory", "maxMemoryPolicy", "notifyKeyspaceEvents", "databases", "persistenceDisabled"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -2313,6 +2336,13 @@ func (ec *executionContext) unmarshalInputCreateValkeyInput(ctx context.Context, return it, err } it.Databases = data + case "persistenceDisabled": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("persistenceDisabled")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.PersistenceDisabled = data } } return it, nil @@ -2373,7 +2403,7 @@ func (ec *executionContext) unmarshalInputUpdateValkeyInput(ctx context.Context, asMap[k] = v } - fieldsInOrder := [...]string{"name", "environmentName", "teamSlug", "tier", "memory", "maxMemoryPolicy", "notifyKeyspaceEvents", "databases"} + fieldsInOrder := [...]string{"name", "environmentName", "teamSlug", "tier", "memory", "maxMemoryPolicy", "notifyKeyspaceEvents", "databases", "persistenceDisabled"} for _, k := range fieldsInOrder { v, ok := asMap[k] if !ok { @@ -2436,6 +2466,13 @@ func (ec *executionContext) unmarshalInputUpdateValkeyInput(ctx context.Context, return it, err } it.Databases = data + case "persistenceDisabled": + ctx := graphql.WithPathContext(ctx, graphql.NewPathWithField("persistenceDisabled")) + data, err := ec.unmarshalOBoolean2ᚖbool(ctx, v) + if err != nil { + return it, err + } + it.PersistenceDisabled = data } } return it, nil @@ -3017,6 +3054,11 @@ func (ec *executionContext) _Valkey(ctx context.Context, sel ast.SelectionSet, o if out.Values[i] == graphql.Null { atomic.AddUint32(&out.Invalids, 1) } + case "persistenceDisabled": + out.Values[i] = ec._Valkey_persistenceDisabled(ctx, field, obj) + if out.Values[i] == graphql.Null { + atomic.AddUint32(&out.Invalids, 1) + } case "issues": field := field diff --git a/internal/graph/schema/opensearch.graphqls b/internal/graph/schema/opensearch.graphqls index 4e513cb08..4d19e2b92 100644 --- a/internal/graph/schema/opensearch.graphqls +++ b/internal/graph/schema/opensearch.graphqls @@ -82,6 +82,14 @@ type OpenSearch implements Persistence & Node { memory: OpenSearchMemory! "Available storage in GB." storageGB: Int! + "Whether shard indexing back pressure is enabled." + shardIndexingPressureEnabled: Boolean! + "Whether shard indexing back pressure runs in enforced mode. In enforced mode requests that may degrade cluster performance are rejected; in shadow mode (enforced false) metrics are tracked but no requests are rejected." + shardIndexingPressureEnforced: Boolean! + "Maximum number of clauses a Lucene BooleanQuery can contain. When not set, the instance uses the default of 1024. Increasing this value may cause performance issues." + indicesQueryBoolMaxClauseCount: Int + "Maximum content length, in a human-readable quantity (e.g. \"100Mi\", \"1Gi\"), for requests to the OpenSearch HTTP API. When not set, the instance uses the default of 100Mi." + httpMaxContentLength: String "Issues that affects the instance." issues( "Get the first n items in the connection. This can be used in combination with the after parameter." @@ -248,6 +256,14 @@ input CreateOpenSearchInput { version: OpenSearchMajorVersion! "Available storage in GB." storageGB: Int! + "Enable shard indexing back pressure. Defaults to false." + shardIndexingPressureEnabled: Boolean + "Run shard indexing back pressure in enforced mode. Defaults to false (shadow mode)." + shardIndexingPressureEnforced: Boolean + "Maximum number of clauses a Lucene BooleanQuery can contain. Must be between 64 and 4096. When not set, the instance uses the default of 1024." + indicesQueryBoolMaxClauseCount: Int + "Maximum content length for requests to the OpenSearch HTTP API. Specified as a human-readable quantity (e.g. \"100Mi\", \"1Gi\"); unitless values are interpreted as bytes. Must be between 1 byte and 2147483647 bytes (around 2047Mi). When not set, the instance uses the default of 100Mi." + httpMaxContentLength: String } type CreateOpenSearchPayload { @@ -270,6 +286,14 @@ input UpdateOpenSearchInput { version: OpenSearchMajorVersion! "Available storage in GB." storageGB: Int! + "Enable shard indexing back pressure. Defaults to false." + shardIndexingPressureEnabled: Boolean + "Run shard indexing back pressure in enforced mode. Defaults to false (shadow mode)." + shardIndexingPressureEnforced: Boolean + "Maximum number of clauses a Lucene BooleanQuery can contain. Must be between 64 and 4096. When not set, the instance uses the default of 1024." + indicesQueryBoolMaxClauseCount: Int + "Maximum content length for requests to the OpenSearch HTTP API. Specified as a human-readable quantity (e.g. \"100Mi\", \"1Gi\"); unitless values are interpreted as bytes. Must be between 1 byte and 2147483647 bytes (around 2047Mi). When not set, the instance uses the default of 100Mi." + httpMaxContentLength: String } type UpdateOpenSearchPayload { diff --git a/internal/graph/schema/valkey.graphqls b/internal/graph/schema/valkey.graphqls index e19075070..747916d7c 100644 --- a/internal/graph/schema/valkey.graphqls +++ b/internal/graph/schema/valkey.graphqls @@ -86,6 +86,8 @@ type Valkey implements Persistence & Node { notifyKeyspaceEvents: String "Number of databases the Valkey instance is configured with. Default is 16. Minimum 1, maximum 128. Changing this will cause a restart of the Valkey service." databases: Int! + "Whether persistence (RDB dumps and backups) is disabled. If true, all data is lost if the instance is restarted for any reason." + persistenceDisabled: Boolean! "Issues that affects the instance." issues( "Get the first n items in the connection. This can be used in combination with the after parameter." @@ -271,6 +273,8 @@ input CreateValkeyInput { notifyKeyspaceEvents: String "Number of databases. Default is 16. Minimum 1, maximum 128. Changing this will cause a restart of the Valkey service." databases: Int + "Disable persistence (RDB dumps and backups). Defaults to false." + persistenceDisabled: Boolean } type CreateValkeyPayload { @@ -295,6 +299,8 @@ input UpdateValkeyInput { notifyKeyspaceEvents: String "Number of databases. Default is 16. Minimum 1, maximum 128. Changing this will cause a restart of the Valkey service." databases: Int + "Disable persistence (RDB dumps and backups). Defaults to false." + persistenceDisabled: Boolean } type UpdateValkeyPayload { diff --git a/internal/graph/servicemaintenance.resolvers.go b/internal/graph/servicemaintenance.resolvers.go index 403191feb..53aa00d02 100644 --- a/internal/graph/servicemaintenance.resolvers.go +++ b/internal/graph/servicemaintenance.resolvers.go @@ -11,6 +11,7 @@ import ( "github.com/nais/api/internal/persistence/opensearch" "github.com/nais/api/internal/persistence/valkey" "github.com/nais/api/internal/servicemaintenance" + "github.com/nais/api/internal/thirdparty/aiven" ) func (r *mutationResolver) StartValkeyMaintenance(ctx context.Context, input servicemaintenance.StartValkeyMaintenanceInput) (*servicemaintenance.StartValkeyMaintenancePayload, error) { @@ -42,8 +43,12 @@ func (r *mutationResolver) StartOpenSearchMaintenance(ctx context.Context, input } func (r *openSearchResolver) Maintenance(ctx context.Context, obj *opensearch.OpenSearch) (*servicemaintenance.OpenSearchMaintenance, error) { + project, err := aiven.GetProject(ctx, obj.EnvironmentName) + if err != nil { + return nil, err + } return &servicemaintenance.OpenSearchMaintenance{ - AivenProject: obj.AivenProject, + AivenProject: project.ID, ServiceName: obj.FullyQualifiedName(), }, nil } @@ -88,8 +93,12 @@ func (r *openSearchMaintenanceResolver) Updates(ctx context.Context, obj *servic } func (r *valkeyResolver) Maintenance(ctx context.Context, obj *valkey.Valkey) (*servicemaintenance.ValkeyMaintenance, error) { + project, err := aiven.GetProject(ctx, obj.EnvironmentName) + if err != nil { + return nil, err + } return &servicemaintenance.ValkeyMaintenance{ - AivenProject: obj.AivenProject, + AivenProject: project.ID, ServiceName: obj.FullyQualifiedName(), }, nil } diff --git a/internal/kubernetes/fake/fake.go b/internal/kubernetes/fake/fake.go index 234fdfc1f..bb0e3848a 100644 --- a/internal/kubernetes/fake/fake.go +++ b/internal/kubernetes/fake/fake.go @@ -16,6 +16,7 @@ import ( liberator_aiven_io_v1alpha1 "github.com/nais/liberator/pkg/apis/aiven.io/v1alpha1" nais_io_v1alpha1 "github.com/nais/liberator/pkg/apis/nais.io/v1alpha1" data_nais_io_v1 "github.com/nais/pgrator/pkg/api/datav1" + mapperatorv1 "github.com/nais/pgrator/pkg/api/v1" unleash_nais_io_v1 "github.com/nais/unleasherator/api/v1" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -217,6 +218,8 @@ func NewDynamicClient(scheme *runtime.Scheme) *dynfake.FakeDynamicClient { unleash_nais_io_v1.GroupVersion.WithResource("remoteunleashes"): "RemoteUnleashList", data_nais_io_v1.GroupVersion.WithResource("postgres"): "PostgresList", nais_io_v1alpha1.GroupVersion.WithResource("tunnels"): "TunnelList", + mapperatorv1.GroupVersion.WithResource("valkeys"): "ValkeyList", + mapperatorv1.GroupVersion.WithResource("opensearches"): "OpenSearchList", }) client.PrependReactor("patch", "*", func(action k8stesting.Action) (handled bool, ret runtime.Object, err error) { diff --git a/internal/kubernetes/scheme.go b/internal/kubernetes/scheme.go index dbbf03e08..60e0a63e2 100644 --- a/internal/kubernetes/scheme.go +++ b/internal/kubernetes/scheme.go @@ -10,6 +10,7 @@ import ( nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1" nais_io_v1alpha1 "github.com/nais/liberator/pkg/apis/nais.io/v1alpha1" data_nais_io_v1 "github.com/nais/pgrator/pkg/api/datav1" + mapperatorv1 "github.com/nais/pgrator/pkg/api/v1" unleash_nais_io_v1 "github.com/nais/unleasherator/api/v1" appsv1 "k8s.io/api/apps/v1" authorizationv1 "k8s.io/api/authorization/v1" @@ -40,6 +41,7 @@ func NewScheme() (*runtime.Scheme, error) { aiven_nais_io_v1.AddToScheme, data_nais_io_v1.AddToScheme, authorizationv1.AddToScheme, + mapperatorv1.AddToScheme, } for _, f := range funcs { diff --git a/internal/kubernetes/utils.go b/internal/kubernetes/utils.go new file mode 100644 index 000000000..4c49491ca --- /dev/null +++ b/internal/kubernetes/utils.go @@ -0,0 +1,24 @@ +package kubernetes + +import ( + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" +) + +func ToUnstructured(obj any) (*unstructured.Unstructured, error) { + mp, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) + if err != nil { + return nil, err + } + + return &unstructured.Unstructured{Object: mp}, nil +} + +func ToConcrete[T any](u *unstructured.Unstructured) (*T, error) { + var obj T + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &obj); err != nil { + return nil, err + } + + return &obj, nil +} diff --git a/internal/kubernetes/watcher/cluster_watcher.go b/internal/kubernetes/watcher/cluster_watcher.go index 1815b075a..4eb9e4d53 100644 --- a/internal/kubernetes/watcher/cluster_watcher.go +++ b/internal/kubernetes/watcher/cluster_watcher.go @@ -135,6 +135,7 @@ func (w *clusterWatcher[T]) OnDelete(obj any) { w.watcher.remove(w.cluster, t) } +// Delete will delete the resource using an imperonated client. func (w *clusterWatcher[T]) Delete(ctx context.Context, namespace, name string) error { client, err := w.ImpersonatedClient(ctx) if err != nil { diff --git a/internal/kubernetes/watcher/watcher.go b/internal/kubernetes/watcher/watcher.go index 2f1bd0dd0..5c567fd53 100644 --- a/internal/kubernetes/watcher/watcher.go +++ b/internal/kubernetes/watcher/watcher.go @@ -276,6 +276,7 @@ func (w *Watcher[T]) GetByNamespace(namespace string, filter ...Filter) []*Envir return ret } +// Delete will delete the resource using an imperonated client. func (w *Watcher[T]) Delete(ctx context.Context, cluster, namespace string, name string) error { for _, watcher := range w.watchers { if watcher.cluster == cluster { diff --git a/internal/kubernetes/watchers/watchers.go b/internal/kubernetes/watchers/watchers.go index 3b5b2f133..cfda14f0b 100644 --- a/internal/kubernetes/watchers/watchers.go +++ b/internal/kubernetes/watchers/watchers.go @@ -35,6 +35,7 @@ type ( BqWatcher = watcher.Watcher[*bigquery.BigQueryDataset] ValkeyWatcher = watcher.Watcher[*valkey.Valkey] OpenSearchWatcher = watcher.Watcher[*opensearch.OpenSearch] + NaisOpenSearchWatcher = watcher.Watcher[*opensearch.OpenSearch] BucketWatcher = watcher.Watcher[*bucket.Bucket] SqlDatabaseWatcher = watcher.Watcher[*sqlinstance.SQLDatabase] SqlInstanceWatcher = watcher.Watcher[*sqlinstance.SQLInstance] @@ -48,6 +49,7 @@ type ( ConfigWatcher = watcher.Watcher[*config.Config] ReplicaSetWatcher = watcher.Watcher[*appsv1.ReplicaSet] TunnelWatcher = watcher.Watcher[*tunnel.Tunnel] + NaisValkeyWatcher = watcher.Watcher[*valkey.Valkey] ) type Watchers struct { @@ -57,6 +59,7 @@ type Watchers struct { BqWatcher *BqWatcher ValkeyWatcher *ValkeyWatcher OpenSearchWatcher *OpenSearchWatcher + NaisOpenSearchWatcher *NaisOpenSearchWatcher BucketWatcher *BucketWatcher SqlDatabaseWatcher *SqlDatabaseWatcher SqlInstanceWatcher *SqlInstanceWatcher @@ -70,6 +73,7 @@ type Watchers struct { ConfigWatcher *ConfigWatcher ReplicaSetWatcher *ReplicaSetWatcher TunnelWatcher *TunnelWatcher + NaisValkeyWatcher *NaisValkeyWatcher } func SetupWatchers( @@ -84,6 +88,7 @@ func SetupWatchers( BqWatcher: bigquery.NewWatcher(ctx, watcherMgr), ValkeyWatcher: valkey.NewWatcher(ctx, watcherMgr), OpenSearchWatcher: opensearch.NewWatcher(ctx, watcherMgr), + NaisOpenSearchWatcher: opensearch.NewNaisOpenSearchWatcher(ctx, watcherMgr), BucketWatcher: bucket.NewWatcher(ctx, watcherMgr), SqlDatabaseWatcher: sqlinstance.NewDatabaseWatcher(ctx, watcherMgr), SqlInstanceWatcher: sqlinstance.NewInstanceWatcher(ctx, watcherMgr), @@ -97,5 +102,6 @@ func SetupWatchers( ConfigWatcher: config.NewWatcher(ctx, watcherMgr), ReplicaSetWatcher: instancegroup.NewWatcher(ctx, watcherMgr), TunnelWatcher: tunnel.NewWatcher(ctx, watcherMgr), + NaisValkeyWatcher: valkey.NewNaisValkeyWatcher(ctx, watcherMgr), } } diff --git a/internal/persistence/opensearch/client.go b/internal/persistence/opensearch/client.go index 344baf330..9a0e0b4b2 100644 --- a/internal/persistence/opensearch/client.go +++ b/internal/persistence/opensearch/client.go @@ -3,16 +3,13 @@ package opensearch import ( "context" - "github.com/nais/api/internal/kubernetes/watcher" "github.com/nais/api/internal/slug" "github.com/nais/api/internal/workload" "github.com/nais/api/internal/workload/application" "github.com/nais/api/internal/workload/job" ) -type client struct { - watcher *watcher.Watcher[*OpenSearch] -} +type client struct{} func instanceNamer(teamSlug slug.Slug, instanceName string) string { return NamePrefix(teamSlug) + instanceName diff --git a/internal/persistence/opensearch/dataloader.go b/internal/persistence/opensearch/dataloader.go index 474d6deba..64d6b54d0 100644 --- a/internal/persistence/opensearch/dataloader.go +++ b/internal/persistence/opensearch/dataloader.go @@ -4,13 +4,17 @@ import ( "context" "github.com/nais/api/internal/graph/loader" + "github.com/nais/api/internal/kubernetes" "github.com/nais/api/internal/kubernetes/watcher" + "github.com/nais/api/internal/slug" "github.com/nais/api/internal/thirdparty/aiven" + naiscrd "github.com/nais/pgrator/pkg/api/v1" "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" "github.com/vikstrous/dataloadgen" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" ) type ctxKey int @@ -22,8 +26,8 @@ type AivenDataLoaderKey struct { ServiceName string } -func NewLoaderContext(ctx context.Context, tenantName string, watcher *watcher.Watcher[*OpenSearch], aivenClient aiven.AivenClient, logger logrus.FieldLogger) context.Context { - return context.WithValue(ctx, loadersKey, newLoaders(tenantName, watcher, aivenClient, logger)) +func NewLoaderContext(ctx context.Context, tenantName string, openSearchWatcher, naisOpenSearchWatcher *watcher.Watcher[*OpenSearch], aivenClient aiven.AivenClient, logger logrus.FieldLogger) context.Context { + return context.WithValue(ctx, loadersKey, newLoaders(tenantName, openSearchWatcher, naisOpenSearchWatcher, aivenClient, logger)) } func NewWatcher(ctx context.Context, mgr *watcher.Manager) *watcher.Watcher[*OpenSearch] { @@ -42,6 +46,26 @@ func NewWatcher(ctx context.Context, mgr *watcher.Manager) *watcher.Watcher[*Ope return w } +func NewNaisOpenSearchWatcher(ctx context.Context, mgr *watcher.Manager) *watcher.Watcher[*OpenSearch] { + w := watcher.Watch(mgr, &OpenSearch{}, watcher.WithConverter(func(o *unstructured.Unstructured, environmentName string) (obj any, ok bool) { + v, err := kubernetes.ToConcrete[naiscrd.OpenSearch](o) + if err != nil { + return nil, false + } + ret, err := toOpenSearchFromNais(v, environmentName) + if err != nil { + return nil, false + } + return ret, true + }), watcher.WithGVR(schema.GroupVersionResource{ + Group: "nais.io", + Version: "v1", + Resource: "opensearches", + })) + w.Start(ctx) + return w +} + func fromContext(ctx context.Context) *loaders { return ctx.Value(loadersKey).(*loaders) } @@ -49,27 +73,38 @@ func fromContext(ctx context.Context) *loaders { type loaders struct { client *client watcher *watcher.Watcher[*OpenSearch] + naisWatcher *watcher.Watcher[*OpenSearch] versionLoader *dataloadgen.Loader[*AivenDataLoaderKey, string] tenantName string aivenClient aiven.AivenClient } -func newLoaders(tenantName string, watcher *watcher.Watcher[*OpenSearch], aivenClient aiven.AivenClient, logger logrus.FieldLogger) *loaders { - client := &client{ - watcher: watcher, - } +func newLoaders(tenantName string, watcher, naisOpenSearchWatcher *watcher.Watcher[*OpenSearch], aivenClient aiven.AivenClient, logger logrus.FieldLogger) *loaders { + client := &client{} versionLoader := &dataloader{aivenClient: aivenClient, log: logger} return &loaders{ client: client, watcher: watcher, + naisWatcher: naisOpenSearchWatcher, tenantName: tenantName, versionLoader: dataloadgen.NewLoader(versionLoader.getVersions, loader.DefaultDataLoaderOptions...), aivenClient: aivenClient, } } +func newK8sClient(ctx context.Context, environmentName string, teamSlug slug.Slug) (dynamic.ResourceInterface, error) { + sysClient, err := fromContext(ctx).naisWatcher.SystemAuthenticatedClient( + ctx, + environmentName, + ) + if err != nil { + return nil, err + } + return sysClient.Namespace(teamSlug.String()), nil +} + type dataloader struct { aivenClient aiven.AivenClient log logrus.FieldLogger diff --git a/internal/persistence/opensearch/models.go b/internal/persistence/opensearch/models.go index 88253f46c..872e52b46 100644 --- a/internal/persistence/opensearch/models.go +++ b/internal/persistence/opensearch/models.go @@ -19,6 +19,9 @@ import ( "github.com/nais/api/internal/validate" "github.com/nais/api/internal/workload" aiven_io_v1alpha1 "github.com/nais/liberator/pkg/apis/aiven.io/v1alpha1" + "github.com/nais/pgrator/pkg/api" + naiscrd "github.com/nais/pgrator/pkg/api/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -51,17 +54,22 @@ type OpenSearchTierFacetItem struct { } type OpenSearch struct { - Name string `json:"name"` - Status *OpenSearchStatus `json:"status"` - TerminationProtection bool `json:"terminationProtection"` - Tier OpenSearchTier `json:"tier"` - Memory OpenSearchMemory `json:"memory"` - StorageGB StorageGB `json:"storageGB"` - TeamSlug slug.Slug `json:"-"` - EnvironmentName string `json:"-"` - WorkloadReference *workload.Reference `json:"-"` - AivenProject string `json:"-"` - MajorVersion OpenSearchMajorVersion `json:"-"` + Name string `json:"name"` + Status *naiscrd.OpenSearchStatus `json:"status"` + TerminationProtection bool `json:"terminationProtection"` + Tier OpenSearchTier `json:"tier"` + Memory OpenSearchMemory `json:"memory"` + StorageGB StorageGB `json:"storageGB"` + + ShardIndexingPressureEnabled bool `json:"shardIndexingPressureEnabled"` + ShardIndexingPressureEnforced bool `json:"shardIndexingPressureEnforced"` + IndicesQueryBoolMaxClauseCount *int `json:"indicesQueryBoolMaxClauseCount,omitempty"` + HTTPMaxContentLength *string `json:"httpMaxContentLength,omitempty"` + + TeamSlug slug.Slug `json:"-"` + EnvironmentName string `json:"-"` + WorkloadReference *workload.Reference `json:"-"` + MajorVersion OpenSearchMajorVersion `json:"-"` } func (OpenSearch) IsPersistence() {} @@ -177,6 +185,14 @@ func toOpenSearch(u *unstructured.Unstructured, envName string) (*OpenSearch, er return nil, fmt.Errorf("converting to OpenSearch: %w", err) } + if len(obj.GetOwnerReferences()) > 0 { + for _, ownerRef := range obj.GetOwnerReferences() { + if ownerRef.Kind == "OpenSearch" { + return nil, fmt.Errorf("skipping OpenSearch %s in namespace %s because it has an owner reference", obj.GetName(), obj.GetNamespace()) + } + } + } + // Liberator doesn't contain this field, so we read it directly from the unstructured object terminationProtection, _, _ := unstructured.NestedBool(u.Object, specTerminationProtection...) @@ -207,21 +223,77 @@ func toOpenSearch(u *unstructured.Unstructured, envName string) (*OpenSearch, er } } + shardIndexingPressureEnabled, _, _ := unstructured.NestedBool(u.Object, specShardIndexingPressureEnabled...) + shardIndexingPressureEnforced, _, _ := unstructured.NestedBool(u.Object, specShardIndexingPressureEnforced...) + + var queryBoolMaxClauseCount *int + if v, found, _ := unstructured.NestedNumberAsFloat64(u.Object, specIndicesQueryBoolMaxClauseCount...); found { + n := int(v) + queryBoolMaxClauseCount = &n + } + + var maxContentLength *string + if v, found, _ := unstructured.NestedNumberAsFloat64(u.Object, specHTTPMaxContentLength...); found { + s := resource.NewQuantity(int64(v), resource.BinarySI).String() + maxContentLength = &s + } + return &OpenSearch{ Name: name, EnvironmentName: envName, TerminationProtection: terminationProtection, - Status: &OpenSearchStatus{ - Conditions: obj.Status.Conditions, - State: obj.Status.State, + Status: &naiscrd.OpenSearchStatus{ + BaseStatus: api.BaseStatus{ + Conditions: obj.Status.Conditions, + }, }, - TeamSlug: slug.Slug(obj.GetNamespace()), - WorkloadReference: workload.ReferenceFromOwnerReferences(obj.GetOwnerReferences()), - AivenProject: obj.Spec.Project, - Tier: machine.Tier, - Memory: machine.Memory, - MajorVersion: majorVersion, - StorageGB: storageGB, + TeamSlug: slug.Slug(obj.GetNamespace()), + WorkloadReference: workload.ReferenceFromOwnerReferences(obj.GetOwnerReferences()), + Tier: machine.Tier, + Memory: machine.Memory, + MajorVersion: majorVersion, + StorageGB: storageGB, + ShardIndexingPressureEnabled: shardIndexingPressureEnabled, + ShardIndexingPressureEnforced: shardIndexingPressureEnforced, + IndicesQueryBoolMaxClauseCount: queryBoolMaxClauseCount, + HTTPMaxContentLength: maxContentLength, + }, nil +} + +func toOpenSearchFromNais(o *naiscrd.OpenSearch, envName string) (*OpenSearch, error) { + majorVersion := fromMapperatorVersion(o.Spec.Version) + + var shardIndexingPressureEnabled, shardIndexingPressureEnforced bool + if o.Spec.ShardIndexingPressure != nil { + shardIndexingPressureEnabled = o.Spec.ShardIndexingPressure.Enabled + shardIndexingPressureEnforced = o.Spec.ShardIndexingPressure.Enforced + } + + var queryBoolMaxClauseCount *int + if o.Spec.Indices != nil { + queryBoolMaxClauseCount = o.Spec.Indices.QueryBoolMaxClauseCount + } + + var maxContentLength *string + if o.Spec.Http != nil && o.Spec.Http.MaxContentLength != nil { + s := o.Spec.Http.MaxContentLength.String() + maxContentLength = &s + } + + return &OpenSearch{ + Name: o.Name, + EnvironmentName: envName, + Status: o.Status, + TeamSlug: slug.Slug(o.Namespace), + WorkloadReference: workload.ReferenceFromOwnerReferences(o.OwnerReferences), + Tier: fromMapperatorTier(o.Spec.Tier), + Memory: fromMapperatorMemory(o.Spec.Memory), + MajorVersion: majorVersion, + StorageGB: StorageGB(o.Spec.StorageGB), + ShardIndexingPressureEnabled: shardIndexingPressureEnabled, + ShardIndexingPressureEnforced: shardIndexingPressureEnforced, + IndicesQueryBoolMaxClauseCount: queryBoolMaxClauseCount, + HTTPMaxContentLength: maxContentLength, }, nil } @@ -262,10 +334,14 @@ func (o *OpenSearchMetadataInput) ValidationErrors(ctx context.Context) *validat type OpenSearchInput struct { OpenSearchMetadataInput - Tier OpenSearchTier `json:"tier"` - Memory OpenSearchMemory `json:"memory"` - Version OpenSearchMajorVersion `json:"version"` - StorageGB StorageGB `json:"storageGB"` + Tier OpenSearchTier `json:"tier"` + Memory OpenSearchMemory `json:"memory"` + Version OpenSearchMajorVersion `json:"version"` + StorageGB StorageGB `json:"storageGB"` + ShardIndexingPressureEnabled *bool `json:"shardIndexingPressureEnabled,omitempty"` + ShardIndexingPressureEnforced *bool `json:"shardIndexingPressureEnforced,omitempty"` + IndicesQueryBoolMaxClauseCount *int `json:"indicesQueryBoolMaxClauseCount,omitempty"` + HTTPMaxContentLength *string `json:"httpMaxContentLength,omitempty"` } func (o *OpenSearchInput) Validate(ctx context.Context) error { @@ -281,6 +357,21 @@ func (o *OpenSearchInput) Validate(ctx context.Context) error { verr.Add("version", "Invalid OpenSearch version: %s.", o.Version.String()) } + if o.IndicesQueryBoolMaxClauseCount != nil { + if c := *o.IndicesQueryBoolMaxClauseCount; c < 64 || c > 4096 { + verr.Add("indicesQueryBoolMaxClauseCount", "Query bool max clause count must be between 64 and 4096.") + } + } + + if o.HTTPMaxContentLength != nil { + q, err := resource.ParseQuantity(*o.HTTPMaxContentLength) + if err != nil { + verr.Add("httpMaxContentLength", "Max content length must be a valid quantity (e.g. \"100Mi\", \"1Gi\").") + } else if b := q.Value(); b < 1 || b > 2147483647 { + verr.Add("httpMaxContentLength", "Max content length must be between 1 byte and 2147483647 bytes (around 2047Mi).") + } + } + machine, err := machineTypeFromTierAndMemory(o.Tier, o.Memory) if err != nil { verr.Add("memory", "%s", err) diff --git a/internal/persistence/opensearch/queries.go b/internal/persistence/opensearch/queries.go index f2a89ce04..c76ed6c20 100644 --- a/internal/persistence/opensearch/queries.go +++ b/internal/persistence/opensearch/queries.go @@ -2,6 +2,7 @@ package opensearch import ( "context" + "errors" "fmt" "strconv" "strings" @@ -19,16 +20,25 @@ import ( "github.com/nais/api/internal/slug" "github.com/nais/api/internal/thirdparty/aiven" nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1" + "github.com/nais/pgrator/pkg/api" + naiscrd "github.com/nais/pgrator/pkg/api/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/utils/ptr" ) var ( specDiskSpace = []string{"spec", "disk_space"} - specPlan = []string{"spec", "plan"} specTerminationProtection = []string{"spec", "terminationProtection"} specOpenSearchVersion = []string{"spec", "userConfig", "opensearch_version"} + + specShardIndexingPressureEnabled = []string{"spec", "userConfig", "opensearch", "shard_indexing_pressure", "enabled"} + specShardIndexingPressureEnforced = []string{"spec", "userConfig", "opensearch", "shard_indexing_pressure", "enforced"} + + specIndicesQueryBoolMaxClauseCount = []string{"spec", "userConfig", "opensearch", "indices_query_bool_max_clause_count"} + + specHTTPMaxContentLength = []string{"spec", "userConfig", "opensearch", "http_max_content_length"} ) func GetByIdent(ctx context.Context, id ident.Ident) (*OpenSearch, error) { @@ -41,15 +51,23 @@ func GetByIdent(ctx context.Context, id ident.Ident) (*OpenSearch, error) { } func Get(ctx context.Context, teamSlug slug.Slug, environment, name string) (*OpenSearch, error) { - prefix := instanceNamer(teamSlug, "") - if !strings.HasPrefix(name, prefix) { - name = instanceNamer(teamSlug, name) + v, err := fromContext(ctx).naisWatcher.Get(environment, teamSlug.String(), name) + if errors.Is(err, &watcher.ErrorNotFound{}) { + prefix := instanceNamer(teamSlug, "") + if !strings.HasPrefix(name, prefix) { + name = instanceNamer(teamSlug, name) + } + v, err = fromContext(ctx).watcher.Get(environment, teamSlug.String(), name) } - return fromContext(ctx).client.watcher.Get(environment, teamSlug.String(), name) + return v, err } func State(ctx context.Context, os *OpenSearch) (OpenSearchState, error) { - s, err := fromContext(ctx).aivenClient.ServiceGet(ctx, os.AivenProject, os.FullyQualifiedName()) + project, err := aiven.GetProject(ctx, os.EnvironmentName) + if err != nil { + return OpenSearchStateUnknown, err + } + s, err := fromContext(ctx).aivenClient.ServiceGet(ctx, project.ID, os.FullyQualifiedName()) if err != nil { // The OpenSearch instance may not have been created in Aiven yet, or it has been deleted. // In both cases, we return "unknown" state rather than an error. @@ -87,7 +105,9 @@ func ListForTeam(ctx context.Context, teamSlug slug.Slug, page *pagination.Pagin } func ListAllForTeam(ctx context.Context, teamSlug slug.Slug) []*OpenSearch { - all := fromContext(ctx).client.watcher.GetByNamespace(teamSlug.String(), watcher.WithoutDeleted()) + all := fromContext(ctx).watcher.GetByNamespace(teamSlug.String(), watcher.WithoutDeleted()) + allNais := fromContext(ctx).naisWatcher.GetByNamespace(teamSlug.String(), watcher.WithoutDeleted()) + all = append(all, allNais...) return watcher.Objects(all) } @@ -121,8 +141,12 @@ func ListAccess(ctx context.Context, openSearch *OpenSearch, page *pagination.Pa } func GetOpenSearchVersion(ctx context.Context, os *OpenSearch) (*OpenSearchVersion, error) { + project, err := aiven.GetProject(ctx, os.EnvironmentName) + if err != nil { + return nil, err + } key := AivenDataLoaderKey{ - Project: os.AivenProject, + Project: project.ID, ServiceName: os.FullyQualifiedName(), } @@ -141,7 +165,7 @@ func GetOpenSearchVersion(ctx context.Context, os *OpenSearch) (*OpenSearchVersi } if major == "" { - major = OpenSearchMajorVersionV2 + major = OpenSearchMajorVersionV3_3 } return &OpenSearchVersion{ @@ -155,7 +179,7 @@ func GetForWorkload(ctx context.Context, teamSlug slug.Slug, environment string, return nil, nil } - return fromContext(ctx).client.watcher.Get(environment, teamSlug.String(), instanceNamer(teamSlug, reference.Instance)) + return Get(ctx, teamSlug, environment, reference.Instance) } func Create(ctx context.Context, input CreateOpenSearchInput) (*CreateOpenSearchPayload, error) { @@ -163,64 +187,72 @@ func Create(ctx context.Context, input CreateOpenSearchInput) (*CreateOpenSearch return nil, err } - namespace := input.TeamSlug.String() - client, err := fromContext(ctx).watcher.ImpersonatedClientWithNamespace(ctx, input.EnvironmentName, namespace) + client, err := newK8sClient(ctx, input.EnvironmentName, input.TeamSlug) if err != nil { return nil, err } - machine, err := machineTypeFromTierAndMemory(input.Tier, input.Memory) - if err != nil { - return nil, err + // Ensure there's no existing Aiven OpenSearch with the same name + // This can be removed when we manage all opensearches through Console + _, err = fromContext(ctx).watcher.Get(input.EnvironmentName, input.TeamSlug.String(), instanceNamer(input.TeamSlug, input.Name)) + if !errors.Is(err, &watcher.ErrorNotFound{}) { + return nil, apierror.Errorf("OpenSearch with the name %q already exists, but are not yet managed through Console.", input.Name) } - res := &unstructured.Unstructured{} - res.SetAPIVersion("aiven.io/v1alpha1") - res.SetKind("OpenSearch") - res.SetName(instanceNamer(input.TeamSlug, input.Name)) - res.SetNamespace(namespace) + res := &naiscrd.OpenSearch{ + TypeMeta: metav1.TypeMeta{ + Kind: "OpenSearch", + APIVersion: "nais.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: input.Name, + Namespace: input.TeamSlug.String(), + }, + Spec: naiscrd.OpenSearchSpec{ + Tier: toMapperatorTier(input.Tier), + Memory: toMapperatorMemory(input.Memory), + Version: toMapperatorVersion(input.Version), + StorageGB: int(input.StorageGB), + }, + } res.SetAnnotations(kubernetes.WithCommonAnnotations(nil, authz.ActorFromContext(ctx).User.Identity())) kubernetes.SetManagedByConsoleLabel(res) - aivenProject, err := aiven.GetProject(ctx, input.EnvironmentName) - if err != nil { - return nil, err + if input.ShardIndexingPressureEnabled != nil || input.ShardIndexingPressureEnforced != nil { + res.Spec.ShardIndexingPressure = &naiscrd.OpenSearchShardIndexingPressure{ + Enabled: ptr.Deref(input.ShardIndexingPressureEnabled, false), + Enforced: ptr.Deref(input.ShardIndexingPressureEnforced, false), + } } - version, err := input.Version.ToAivenString() - if err != nil { - return nil, err + + if input.IndicesQueryBoolMaxClauseCount != nil { + res.Spec.Indices = &naiscrd.OpenSearchIndices{ + QueryBoolMaxClauseCount: input.IndicesQueryBoolMaxClauseCount, + } } - res.Object["spec"] = map[string]any{ - "cloudName": "google-europe-north1", - "plan": machine.AivenPlan, - "project": aivenProject.ID, - "projectVpcId": aivenProject.VPC, - "disk_space": input.StorageGB.ToAivenString(), - "terminationProtection": true, - "tags": map[string]any{ - "environment": input.EnvironmentName, - "team": namespace, - "tenant": fromContext(ctx).tenantName, - }, - "userConfig": map[string]any{ - "opensearch_version": version, - }, + if input.HTTPMaxContentLength != nil { + q, err := resource.ParseQuantity(*input.HTTPMaxContentLength) + if err != nil { + return nil, err + } + res.Spec.Http = &naiscrd.OpenSearchHttp{ + MaxContentLength: &q, + } } - ret, err := client.Create(ctx, res, metav1.CreateOptions{}) + obj, err := kubernetes.ToUnstructured(res) if err != nil { + return nil, err + } + + if _, err = client.Create(ctx, obj, metav1.CreateOptions{}); err != nil { if k8serrors.IsAlreadyExists(err) { return nil, apierror.ErrAlreadyExists } return nil, err } - err = aiven.UpsertPrometheusServiceIntegration(ctx, fromContext(ctx).watcher, ret, aivenProject, input.EnvironmentName) - if err != nil { - return nil, fmt.Errorf("creating Prometheus service integration: %w", err) - } - err = activitylog.Create(ctx, activitylog.CreateInput{ Action: activitylog.ActivityLogEntryActionCreated, Actor: authz.ActorFromContext(ctx).User, @@ -233,7 +265,7 @@ func Create(ctx context.Context, input CreateOpenSearchInput) (*CreateOpenSearch return nil, err } - os, err := toOpenSearch(ret, input.EnvironmentName) + os, err := toOpenSearchFromNais(res, input.EnvironmentName) if err != nil { return nil, err } @@ -248,41 +280,41 @@ func Update(ctx context.Context, input UpdateOpenSearchInput) (*UpdateOpenSearch return nil, err } - client, err := fromContext(ctx).watcher.ImpersonatedClientWithNamespace(ctx, input.EnvironmentName, input.TeamSlug.String()) - if err != nil { - return nil, err - } - - openSearch, err := client.Get(ctx, instanceNamer(input.TeamSlug, input.Name), metav1.GetOptions{}) + client, err := newK8sClient(ctx, input.EnvironmentName, input.TeamSlug) if err != nil { return nil, err } - if !kubernetes.HasManagedByConsoleLabel(openSearch) { - return nil, apierror.Errorf("OpenSearch %s/%s is not managed by Console", input.TeamSlug, input.Name) - } - changes := make([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, 0) - - res, err := updatePlan(openSearch, input) + openSearch, err := client.Get(ctx, input.Name, metav1.GetOptions{}) if err != nil { return nil, err } - changes = append(changes, res...) - res, err = updateVersion(ctx, openSearch, input) + concreteOpenSearch, err := kubernetes.ToConcrete[naiscrd.OpenSearch](openSearch) if err != nil { return nil, err } - changes = append(changes, res...) - res, err = updateStorage(openSearch, input) - if err != nil { - return nil, err + changes := make([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, 0) + updateFuncs := []func(*naiscrd.OpenSearch, UpdateOpenSearchInput) ([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, error){ + updateTier, + updateMemory, + updateVersion, + updateStorage, + updateShardIndexingPressure, + updateIndices, + updateHTTP, + } + + for _, f := range updateFuncs { + res, err := f(concreteOpenSearch, input) + if err != nil { + return nil, err + } + changes = append(changes, res...) } - changes = append(changes, res...) if len(changes) == 0 { - // No changes to update os, err := toOpenSearch(openSearch, input.EnvironmentName) if err != nil { return nil, err @@ -293,21 +325,16 @@ func Update(ctx context.Context, input UpdateOpenSearchInput) (*UpdateOpenSearch }, nil } - openSearch.SetAnnotations(kubernetes.WithCommonAnnotations(openSearch.GetAnnotations(), authz.ActorFromContext(ctx).User.Identity())) - - ret, err := client.Update(ctx, openSearch, metav1.UpdateOptions{}) + obj, err := kubernetes.ToUnstructured(concreteOpenSearch) if err != nil { return nil, err } - aivenProject, err := aiven.GetProject(ctx, input.EnvironmentName) - if err != nil { - return nil, err - } + obj.SetAnnotations(kubernetes.WithCommonAnnotations(obj.GetAnnotations(), authz.ActorFromContext(ctx).User.Identity())) - err = aiven.UpsertPrometheusServiceIntegration(ctx, fromContext(ctx).watcher, ret, aivenProject, input.EnvironmentName) + ret, err := client.Update(ctx, obj, metav1.UpdateOptions{}) if err != nil { - return nil, fmt.Errorf("creating Prometheus service integration: %w", err) + return nil, err } err = activitylog.Create(ctx, activitylog.CreateInput{ @@ -325,13 +352,18 @@ func Update(ctx context.Context, input UpdateOpenSearchInput) (*UpdateOpenSearch return nil, err } - os, err := toOpenSearch(ret, input.EnvironmentName) + retOpenSearch, err := kubernetes.ToConcrete[naiscrd.OpenSearch](ret) + if err != nil { + return nil, err + } + + osUpdated, err := toOpenSearchFromNais(retOpenSearch, input.EnvironmentName) if err != nil { return nil, err } return &UpdateOpenSearchPayload{ - OpenSearch: os, + OpenSearch: osUpdated, }, nil } @@ -376,149 +408,210 @@ func CreateOpenSearchCredentials(ctx context.Context, input CreateOpenSearchCred return &CreateOpenSearchCredentialsPayload{Credentials: result.(*OpenSearchCredentials)}, nil } -func updatePlan(openSearch *unstructured.Unstructured, input UpdateOpenSearchInput) ([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, error) { +func updateTier(openSearch *naiscrd.OpenSearch, input UpdateOpenSearchInput) ([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, error) { changes := make([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, 0) - desired, err := machineTypeFromTierAndMemory(input.Tier, input.Memory) - if err != nil { - return nil, err - } - - oldPlan, found, err := unstructured.NestedString(openSearch.Object, specPlan...) - if err != nil { - return nil, err - } - if !found { - // .spec.plan is a required field - return nil, fmt.Errorf("missing .spec.plan in OpenSearch resource") - } - - if oldPlan == desired.AivenPlan { - return changes, nil - } - - oldMachine, err := machineTypeFromPlan(oldPlan) - if err != nil { - return nil, err - } - - if input.Tier != oldMachine.Tier { + origTier := fromMapperatorTier(openSearch.Spec.Tier) + if input.Tier != origTier { changes = append(changes, &OpenSearchUpdatedActivityLogEntryDataUpdatedField{ Field: "tier", - OldValue: new(oldMachine.Tier.String()), + OldValue: new(origTier.String()), NewValue: new(input.Tier.String()), }) } - if input.Memory != oldMachine.Memory { + openSearch.Spec.Tier = toMapperatorTier(input.Tier) + + return changes, nil +} + +func updateMemory(openSearch *naiscrd.OpenSearch, input UpdateOpenSearchInput) ([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, error) { + changes := make([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, 0) + + origMemory := fromMapperatorMemory(openSearch.Spec.Memory) + if input.Memory != origMemory { changes = append(changes, &OpenSearchUpdatedActivityLogEntryDataUpdatedField{ Field: "memory", - OldValue: new(oldMachine.Memory.String()), + OldValue: new(origMemory.String()), NewValue: new(input.Memory.String()), }) } - if err := unstructured.SetNestedField(openSearch.Object, desired.AivenPlan, specPlan...); err != nil { - return nil, err - } + openSearch.Spec.Memory = toMapperatorMemory(input.Memory) + return changes, nil } -func updateVersion(ctx context.Context, openSearch *unstructured.Unstructured, input UpdateOpenSearchInput) ([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, error) { - changes := make([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, 0) +func updateVersion(openSearch *naiscrd.OpenSearch, input UpdateOpenSearchInput) ([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, error) { + origVersion := fromMapperatorVersion(openSearch.Spec.Version) - oldVersion, found, err := unstructured.NestedString(openSearch.Object, specOpenSearchVersion...) - if err != nil { - return nil, err - } - if !found { - os, err := toOpenSearch(openSearch, input.EnvironmentName) - if err != nil { - return nil, err - } - version, err := GetOpenSearchVersion(ctx, os) - if err != nil { - return nil, err - } - - oldVersion = *version.Actual + if origVersion == input.Version { + return nil, nil } - oldMajorVersion, err := OpenSearchMajorVersionFromAivenString(oldVersion) - if err != nil { + if err := input.Version.ValidateUpgradePath(origVersion); err != nil { return nil, err } - if oldMajorVersion == input.Version { - return changes, nil - } + changes := make([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, 0) - if err := input.Version.ValidateUpgradePath(oldMajorVersion); err != nil { - return nil, err + var oldValue *string + if origVersion != "" { + oldValue = new(origVersion.String()) } changes = append(changes, &OpenSearchUpdatedActivityLogEntryDataUpdatedField{ - Field: "version", - OldValue: func() *string { - if found { - return new(oldVersion) - } - return nil - }(), + Field: "version", + OldValue: oldValue, NewValue: new(input.Version.String()), }) - version, err := input.Version.ToAivenString() - if err != nil { - return nil, err + openSearch.Spec.Version = toMapperatorVersion(input.Version) + + return changes, nil +} + +func updateStorage(openSearch *naiscrd.OpenSearch, input UpdateOpenSearchInput) ([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, error) { + oldStorageGB := StorageGB(openSearch.Spec.StorageGB) + + if oldStorageGB == input.StorageGB { + return nil, nil } - if err := unstructured.SetNestedField(openSearch.Object, version, specOpenSearchVersion...); err != nil { - return nil, err + changes := make([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, 0) + + var oldValue *string + if oldStorageGB > 0 { + oldValue = new(oldStorageGB.String()) } + + changes = append(changes, &OpenSearchUpdatedActivityLogEntryDataUpdatedField{ + Field: "storageGB", + OldValue: oldValue, + NewValue: new(input.StorageGB.String()), + }) + + openSearch.Spec.StorageGB = int(input.StorageGB) + return changes, nil } -func updateStorage(openSearch *unstructured.Unstructured, input UpdateOpenSearchInput) ([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, error) { +func updateShardIndexingPressure(openSearch *naiscrd.OpenSearch, input UpdateOpenSearchInput) ([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, error) { + if input.ShardIndexingPressureEnabled == nil && input.ShardIndexingPressureEnforced == nil { + return nil, nil + } + + var oldEnabled, oldEnforced bool + if openSearch.Spec.ShardIndexingPressure != nil { + oldEnabled = openSearch.Spec.ShardIndexingPressure.Enabled + oldEnforced = openSearch.Spec.ShardIndexingPressure.Enforced + } + + newEnabled := ptr.Deref(input.ShardIndexingPressureEnabled, oldEnabled) + newEnforced := ptr.Deref(input.ShardIndexingPressureEnforced, oldEnforced) + changes := make([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, 0) - desired, err := machineTypeFromTierAndMemory(input.Tier, input.Memory) - if err != nil { - return nil, err + if oldEnabled != newEnabled { + changes = append(changes, &OpenSearchUpdatedActivityLogEntryDataUpdatedField{ + Field: "shardIndexingPressureEnabled", + OldValue: new(strconv.FormatBool(oldEnabled)), + NewValue: new(strconv.FormatBool(newEnabled)), + }) + } + if oldEnforced != newEnforced { + changes = append(changes, &OpenSearchUpdatedActivityLogEntryDataUpdatedField{ + Field: "shardIndexingPressureEnforced", + OldValue: new(strconv.FormatBool(oldEnforced)), + NewValue: new(strconv.FormatBool(newEnforced)), + }) + } + + if len(changes) == 0 { + return nil, nil + } + + openSearch.Spec.ShardIndexingPressure = &naiscrd.OpenSearchShardIndexingPressure{ + Enabled: newEnabled, + Enforced: newEnforced, + } + + return changes, nil +} + +func updateIndices(openSearch *naiscrd.OpenSearch, input UpdateOpenSearchInput) ([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, error) { + if input.IndicesQueryBoolMaxClauseCount == nil { + return nil, nil + } + + var oldCount *int + if openSearch.Spec.Indices != nil { + oldCount = openSearch.Spec.Indices.QueryBoolMaxClauseCount + } + newCount := input.IndicesQueryBoolMaxClauseCount + + if equalIntPtr(oldCount, newCount) { + return nil, nil + } + + openSearch.Spec.Indices = &naiscrd.OpenSearchIndices{ + QueryBoolMaxClauseCount: newCount, + } + + return []*OpenSearchUpdatedActivityLogEntryDataUpdatedField{ + { + Field: "indicesQueryBoolMaxClauseCount", + OldValue: intPtrToStringPtr(oldCount), + NewValue: intPtrToStringPtr(newCount), + }, + }, nil +} + +func updateHTTP(openSearch *naiscrd.OpenSearch, input UpdateOpenSearchInput) ([]*OpenSearchUpdatedActivityLogEntryDataUpdatedField, error) { + if input.HTTPMaxContentLength == nil { + return nil, nil } - oldAivenDiskSpace, found, err := unstructured.NestedString(openSearch.Object, specDiskSpace...) + newQ, err := resource.ParseQuantity(*input.HTTPMaxContentLength) if err != nil { return nil, err } - // default to minimum storage capacity for the selected plan, in case the field is not set explicitly - oldStorageGB := desired.StorageMin - if found { - oldStorageGB, err = StorageGBFromAivenString(oldAivenDiskSpace) - if err != nil { - return nil, err + + var oldValue *string + if openSearch.Spec.Http != nil && openSearch.Spec.Http.MaxContentLength != nil { + old := openSearch.Spec.Http.MaxContentLength + if old.Cmp(newQ) == 0 { + return nil, nil } + s := old.String() + oldValue = &s } - if oldStorageGB == input.StorageGB { - return changes, nil + openSearch.Spec.Http = &naiscrd.OpenSearchHttp{ + MaxContentLength: &newQ, } - changes = append(changes, &OpenSearchUpdatedActivityLogEntryDataUpdatedField{ - Field: "storageGB", - OldValue: func() *string { - if found { - return new(oldStorageGB.String()) - } - return nil - }(), - NewValue: new(input.StorageGB.String()), - }) + return []*OpenSearchUpdatedActivityLogEntryDataUpdatedField{ + { + Field: "httpMaxContentLength", + OldValue: oldValue, + NewValue: new(newQ.String()), + }, + }, nil +} - if err := unstructured.SetNestedField(openSearch.Object, input.StorageGB.ToAivenString(), specDiskSpace...); err != nil { - return nil, err +func equalIntPtr(a, b *int) bool { + if a == nil || b == nil { + return a == b } - return changes, nil + return *a == *b +} + +func intPtrToStringPtr(v *int) *string { + if v == nil { + return nil + } + return new(strconv.Itoa(*v)) } func Delete(ctx context.Context, input DeleteOpenSearchInput) (*DeleteOpenSearchPayload, error) { @@ -526,36 +619,35 @@ func Delete(ctx context.Context, input DeleteOpenSearchInput) (*DeleteOpenSearch return nil, err } - name := instanceNamer(input.TeamSlug, input.Name) - client, err := fromContext(ctx).watcher.ImpersonatedClientWithNamespace(ctx, input.EnvironmentName, input.TeamSlug.String()) + client, err := newK8sClient(ctx, input.EnvironmentName, input.TeamSlug) if err != nil { return nil, err } - os, err := client.Get(ctx, name, metav1.GetOptions{}) + openSearch, err := client.Get(ctx, input.Name, metav1.GetOptions{}) if err != nil { return nil, err } - if !kubernetes.HasManagedByConsoleLabel(os) { + if !kubernetes.HasManagedByConsoleLabel(openSearch) { return nil, apierror.Errorf("OpenSearch %s/%s is not managed by Console", input.TeamSlug, input.Name) } - terminationProtection, found, err := unstructured.NestedBool(os.Object, specTerminationProtection...) - if err != nil { - return nil, err + annotations := openSearch.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) } - if found && terminationProtection { - if err := unstructured.SetNestedField(os.Object, false, specTerminationProtection...); err != nil { - return nil, err - } + if annotations[api.AllowDeletionAnnotation] != "true" { + annotations[api.AllowDeletionAnnotation] = "true" + openSearch.SetAnnotations(annotations) - if _, err = client.Update(ctx, os, metav1.UpdateOptions{}); err != nil { - return nil, fmt.Errorf("removing deletion protection: %w", err) + _, err = client.Update(ctx, openSearch, metav1.UpdateOptions{}) + if err != nil { + return nil, fmt.Errorf("set allow deletion annotation: %w", err) } } - if err := fromContext(ctx).watcher.Delete(ctx, input.EnvironmentName, input.TeamSlug.String(), name); err != nil { + if err := client.Delete(ctx, input.Name, metav1.DeleteOptions{}); err != nil { return nil, err } @@ -574,3 +666,93 @@ func Delete(ctx context.Context, input DeleteOpenSearchInput) (*DeleteOpenSearch OpenSearchDeleted: new(true), }, nil } + +func toMapperatorTier(tier OpenSearchTier) naiscrd.OpenSearchTier { + switch tier { + case OpenSearchTierSingleNode: + return naiscrd.OpenSearchTierSingleNode + case OpenSearchTierHighAvailability: + return naiscrd.OpenSearchTierHighAvailability + default: + return "" + } +} + +func fromMapperatorTier(tier naiscrd.OpenSearchTier) OpenSearchTier { + switch tier { + case naiscrd.OpenSearchTierSingleNode: + return OpenSearchTierSingleNode + case naiscrd.OpenSearchTierHighAvailability: + return OpenSearchTierHighAvailability + default: + return "" + } +} + +func toMapperatorMemory(memory OpenSearchMemory) naiscrd.OpenSearchMemory { + switch memory { + case OpenSearchMemoryGB2: + return naiscrd.OpenSearchMemory2GB + case OpenSearchMemoryGB4: + return naiscrd.OpenSearchMemory4GB + case OpenSearchMemoryGB8: + return naiscrd.OpenSearchMemory8GB + case OpenSearchMemoryGB16: + return naiscrd.OpenSearchMemory16GB + case OpenSearchMemoryGB32: + return naiscrd.OpenSearchMemory32GB + case OpenSearchMemoryGB64: + return naiscrd.OpenSearchMemory64GB + default: + return "" + } +} + +func fromMapperatorMemory(memory naiscrd.OpenSearchMemory) OpenSearchMemory { + switch memory { + case naiscrd.OpenSearchMemory2GB: + return OpenSearchMemoryGB2 + case naiscrd.OpenSearchMemory4GB: + return OpenSearchMemoryGB4 + case naiscrd.OpenSearchMemory8GB: + return OpenSearchMemoryGB8 + case naiscrd.OpenSearchMemory16GB: + return OpenSearchMemoryGB16 + case naiscrd.OpenSearchMemory32GB: + return OpenSearchMemoryGB32 + case naiscrd.OpenSearchMemory64GB: + return OpenSearchMemoryGB64 + default: + return "" + } +} + +func toMapperatorVersion(version OpenSearchMajorVersion) naiscrd.OpenSearchVersion { + switch version { + case OpenSearchMajorVersionV1: + return naiscrd.OpenSearchVersionV1 + case OpenSearchMajorVersionV2: + return naiscrd.OpenSearchVersionV2 + case OpenSearchMajorVersionV2_19: + return naiscrd.OpenSearchVersionV2_19 + case OpenSearchMajorVersionV3_3: + return naiscrd.OpenSearchVersionV3_3 + default: + return "" + } +} + +func fromMapperatorVersion(version naiscrd.OpenSearchVersion) OpenSearchMajorVersion { + switch version { + case naiscrd.OpenSearchVersionV1: + return OpenSearchMajorVersionV1 + case naiscrd.OpenSearchVersionV2: + return OpenSearchMajorVersionV2 + case naiscrd.OpenSearchVersionV2_19: + return OpenSearchMajorVersionV2_19 + case naiscrd.OpenSearchVersionV3_3: + return OpenSearchMajorVersionV3_3 + default: + return "" + } +} diff --git a/internal/persistence/valkey/client.go b/internal/persistence/valkey/client.go index a565a151b..571cccdb8 100644 --- a/internal/persistence/valkey/client.go +++ b/internal/persistence/valkey/client.go @@ -3,16 +3,13 @@ package valkey import ( "context" - "github.com/nais/api/internal/kubernetes/watcher" "github.com/nais/api/internal/slug" "github.com/nais/api/internal/workload" "github.com/nais/api/internal/workload/application" "github.com/nais/api/internal/workload/job" ) -type client struct { - watcher *watcher.Watcher[*Valkey] -} +type client struct{} // NamePrefix returns the Kubernetes resource name prefix for Valkey instances // belonging to the given team (e.g. "valkey-myteam-"). diff --git a/internal/persistence/valkey/dataloader.go b/internal/persistence/valkey/dataloader.go index 81cc60a53..372694c2f 100644 --- a/internal/persistence/valkey/dataloader.go +++ b/internal/persistence/valkey/dataloader.go @@ -3,18 +3,22 @@ package valkey import ( "context" + "github.com/nais/api/internal/kubernetes" "github.com/nais/api/internal/kubernetes/watcher" + "github.com/nais/api/internal/slug" "github.com/nais/api/internal/thirdparty/aiven" + naiscrd "github.com/nais/pgrator/pkg/api/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" ) type ctxKey int const loadersKey ctxKey = iota -func NewLoaderContext(ctx context.Context, tenantName string, valkeyWatcher *watcher.Watcher[*Valkey], aivenClient aiven.AivenClient) context.Context { - return context.WithValue(ctx, loadersKey, newLoaders(tenantName, valkeyWatcher, aivenClient)) +func NewLoaderContext(ctx context.Context, tenantName string, valkeyWatcher, naisValkeyWatcher *watcher.Watcher[*Valkey], aivenClient aiven.AivenClient) context.Context { + return context.WithValue(ctx, loadersKey, newLoaders(tenantName, valkeyWatcher, naisValkeyWatcher, aivenClient)) } func NewWatcher(ctx context.Context, mgr *watcher.Manager) *watcher.Watcher[*Valkey] { @@ -33,6 +37,26 @@ func NewWatcher(ctx context.Context, mgr *watcher.Manager) *watcher.Watcher[*Val return w } +func NewNaisValkeyWatcher(ctx context.Context, mgr *watcher.Manager) *watcher.Watcher[*Valkey] { + w := watcher.Watch(mgr, &Valkey{}, watcher.WithConverter(func(o *unstructured.Unstructured, environmentName string) (obj any, ok bool) { + v, err := kubernetes.ToConcrete[naiscrd.Valkey](o) + if err != nil { + return nil, false + } + ret, err := toValkeyFromNais(v, environmentName) + if err != nil { + return nil, false + } + return ret, true + }), watcher.WithGVR(schema.GroupVersionResource{ + Group: "nais.io", + Version: "v1", + Resource: "valkeys", + })) + w.Start(ctx) + return w +} + func fromContext(ctx context.Context) *loaders { return ctx.Value(loadersKey).(*loaders) } @@ -41,18 +65,29 @@ type loaders struct { client *client tenantName string watcher *watcher.Watcher[*Valkey] + naisWatcher *watcher.Watcher[*Valkey] aivenClient aiven.AivenClient } -func newLoaders(tenantName string, watcher *watcher.Watcher[*Valkey], aivenClient aiven.AivenClient) *loaders { - client := &client{ - watcher: watcher, - } +func newLoaders(tenantName string, watcher, naisValkeyWatcher *watcher.Watcher[*Valkey], aivenClient aiven.AivenClient) *loaders { + client := &client{} return &loaders{ client: client, tenantName: tenantName, watcher: watcher, + naisWatcher: naisValkeyWatcher, aivenClient: aivenClient, } } + +func newK8sClient(ctx context.Context, environmentName string, teamSlug slug.Slug) (dynamic.ResourceInterface, error) { + sysClient, err := fromContext(ctx).naisWatcher.SystemAuthenticatedClient( + ctx, + environmentName, + ) + if err != nil { + return nil, err + } + return sysClient.Namespace(teamSlug.String()), nil +} diff --git a/internal/persistence/valkey/machines.go b/internal/persistence/valkey/machines.go index 62353cf30..081caf9b1 100644 --- a/internal/persistence/valkey/machines.go +++ b/internal/persistence/valkey/machines.go @@ -27,24 +27,10 @@ var machineTypes = []machineType{ {AivenPlan: "business-200", Tier: ValkeyTierHighAvailability, Memory: ValkeyMemoryGB200}, } -// tierAndMemory transposes machineTypes for lookup by ValkeyTier and ValkeyMemory -var tierAndMemory map[ValkeyTier]map[ValkeyMemory]machineType - // aivenPlans transposes machineTypes for lookup by an Aiven plan string var aivenPlans map[string]machineType func init() { - tierAndMemory = make(map[ValkeyTier]map[ValkeyMemory]machineType) - for _, m := range machineTypes { - if _, ok := tierAndMemory[m.Tier]; !ok { - tierAndMemory[m.Tier] = make(map[ValkeyMemory]machineType) - } - if _, ok := tierAndMemory[m.Tier][m.Memory]; ok { - panic("duplicate tier and memory combination [" + string(m.Tier) + ", " + string(m.Memory) + "] in machineTypes") - } - tierAndMemory[m.Tier][m.Memory] = m - } - aivenPlans = make(map[string]machineType) for _, m := range machineTypes { if _, ok := aivenPlans[m.AivenPlan]; ok { @@ -54,20 +40,6 @@ func init() { } } -func machineTypeFromTierAndMemory(tier ValkeyTier, memory ValkeyMemory) (*machineType, error) { - memories, ok := tierAndMemory[tier] - if !ok { - return nil, apierror.Errorf("Invalid Valkey tier: %s", tier) - } - - machine, ok := memories[memory] - if !ok { - return nil, apierror.Errorf("Invalid Valkey memory for tier. %v cannot have memory %v", tier, memory) - } - - return &machine, nil -} - func machineTypeFromPlan(plan string) (*machineType, error) { machine, ok := aivenPlans[plan] if !ok { diff --git a/internal/persistence/valkey/models.go b/internal/persistence/valkey/models.go index c96b3e01e..e0d7579e5 100644 --- a/internal/persistence/valkey/models.go +++ b/internal/persistence/valkey/models.go @@ -16,6 +16,8 @@ import ( "github.com/nais/api/internal/validate" "github.com/nais/api/internal/workload" aiven_io_v1alpha1 "github.com/nais/liberator/pkg/apis/aiven.io/v1alpha1" + "github.com/nais/pgrator/pkg/api" + naiscrd "github.com/nais/pgrator/pkg/api/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -49,17 +51,17 @@ type ValkeyTierFacetItem struct { type Valkey struct { Name string `json:"name"` - Status *ValkeyStatus `json:"status"` + Status *naiscrd.ValkeyStatus `json:"status"` TerminationProtection bool `json:"terminationProtection"` Tier ValkeyTier `json:"tier"` Memory ValkeyMemory `json:"memory"` MaxMemoryPolicy ValkeyMaxMemoryPolicy `json:"maxMemoryPolicy,omitempty"` NotifyKeyspaceEvents string `json:"notifyKeyspaceEvents,omitempty"` Databases int `json:"databases"` + PersistenceDisabled bool `json:"persistenceDisabled"` TeamSlug slug.Slug `json:"-"` EnvironmentName string `json:"-"` WorkloadReference *workload.Reference `json:"-"` - AivenProject string `json:"-"` } func (Valkey) IsPersistence() {} @@ -175,11 +177,19 @@ func toValkey(u *unstructured.Unstructured, envName string) (*Valkey, error) { return nil, fmt.Errorf("converting to Valkey instance: %w", err) } + if len(obj.GetOwnerReferences()) > 0 { + for _, ownerRef := range obj.GetOwnerReferences() { + if ownerRef.Kind == "Valkey" { + return nil, fmt.Errorf("skipping Valkey %s in namespace %s because it has an owner reference", obj.GetName(), obj.GetNamespace()) + } + } + } + // Liberator doesn't contain this field, so we read it directly from the unstructured object terminationProtection, _, _ := unstructured.NestedBool(u.Object, specTerminationProtection...) maxMemoryPolicyStr, _, _ := unstructured.NestedString(u.Object, specMaxMemoryPolicy...) - maxMemoryPolicy, err := ValkeyMaxMemoryPolicyFromAivenString(maxMemoryPolicyStr) + maxMemoryPolicy, err := ValkeyMaxMemoryPolicyFromAivenString(naiscrd.ValkeyMaxMemoryPolicy(maxMemoryPolicyStr)) if err != nil { maxMemoryPolicy = "" } @@ -191,6 +201,10 @@ func toValkey(u *unstructured.Unstructured, envName string) (*Valkey, error) { numberOfDatabases = 16 } + // In the Aiven CRD, persistence is configured via userConfig.valkey_persistence, where "off" disables it. + valkeyPersistence, _, _ := unstructured.NestedString(u.Object, specValkeyPersistence...) + persistenceDisabled := valkeyPersistence == "off" + machine, err := machineTypeFromPlan(obj.Spec.Plan) if err != nil { return nil, fmt.Errorf("converting from plan: %w", err) @@ -205,18 +219,54 @@ func toValkey(u *unstructured.Unstructured, envName string) (*Valkey, error) { Name: name, EnvironmentName: envName, TerminationProtection: terminationProtection, - Status: &ValkeyStatus{ - Conditions: obj.Status.Conditions, - State: obj.Status.State, + Status: &naiscrd.ValkeyStatus{ + BaseStatus: api.BaseStatus{ + Conditions: obj.Status.Conditions, + }, }, TeamSlug: slug.Slug(obj.GetNamespace()), WorkloadReference: workload.ReferenceFromOwnerReferences(obj.GetOwnerReferences()), - AivenProject: obj.Spec.Project, Tier: machine.Tier, Memory: machine.Memory, MaxMemoryPolicy: maxMemoryPolicy, NotifyKeyspaceEvents: notifyKeyspaceEvents, Databases: int(numberOfDatabases), + PersistenceDisabled: persistenceDisabled, + }, nil +} + +func toValkeyFromNais(v *naiscrd.Valkey, envName string) (*Valkey, error) { + var mmp ValkeyMaxMemoryPolicy + if v.Spec.MaxMemoryPolicy != "" { + var err error + mmp, err = ValkeyMaxMemoryPolicyFromAivenString(v.Spec.MaxMemoryPolicy) + if err != nil { + return nil, err + } + } + + databases := 16 + if v.Spec.Databases != nil { + databases = *v.Spec.Databases + } + + persistenceDisabled := false + if v.Spec.Persistence != nil { + persistenceDisabled = v.Spec.Persistence.Disabled + } + + return &Valkey{ + Name: v.Name, + EnvironmentName: envName, + Status: v.Status, + TeamSlug: slug.Slug(v.Namespace), + WorkloadReference: workload.ReferenceFromOwnerReferences(v.OwnerReferences), + Tier: fromMapperatorTier(v.Spec.Tier), + Memory: fromMapperatorMemory(v.Spec.Memory), + NotifyKeyspaceEvents: v.Spec.NotifyKeyspaceEvents, + MaxMemoryPolicy: mmp, + Databases: databases, + PersistenceDisabled: persistenceDisabled, }, nil } @@ -262,6 +312,7 @@ type ValkeyInput struct { MaxMemoryPolicy *ValkeyMaxMemoryPolicy `json:"maxMemoryPolicy,omitempty"` NotifyKeyspaceEvents *string `json:"notifyKeyspaceEvents,omitempty"` Databases *int `json:"databases,omitempty"` + PersistenceDisabled *bool `json:"persistenceDisabled,omitempty"` } func (v *ValkeyInput) Validate(ctx context.Context) error { @@ -355,9 +406,9 @@ func (e ValkeyMaxMemoryPolicy) MarshalGQL(w io.Writer) { fmt.Fprint(w, strconv.Quote(e.String())) } -func ValkeyMaxMemoryPolicyFromAivenString(s string) (ValkeyMaxMemoryPolicy, error) { +func ValkeyMaxMemoryPolicyFromAivenString(s naiscrd.ValkeyMaxMemoryPolicy) (ValkeyMaxMemoryPolicy, error) { for _, policy := range AllValkeyMaxMemoryPolicy { - if policy.ToAivenString() == s { + if policy.ToAivenString() == string(s) { return policy, nil } } @@ -367,21 +418,21 @@ func ValkeyMaxMemoryPolicyFromAivenString(s string) (ValkeyMaxMemoryPolicy, erro func (e ValkeyMaxMemoryPolicy) ToAivenString() string { switch e { case ValkeyMaxMemoryPolicyAllkeysLfu: - return "allkeys-lfu" + return string(naiscrd.ValkeyMaxMemoryPolicyAllkeysLFU) case ValkeyMaxMemoryPolicyAllkeysLru: - return "allkeys-lru" + return string(naiscrd.ValkeyMaxMemoryPolicyAllkeysLRU) case ValkeyMaxMemoryPolicyAllkeysRandom: - return "allkeys-random" + return string(naiscrd.ValkeyMaxMemoryPolicyAllkeysRandom) case ValkeyMaxMemoryPolicyNoEviction: - return "noeviction" + return string(naiscrd.ValkeyMaxMemoryPolicyNoEviction) case ValkeyMaxMemoryPolicyVolatileLfu: - return "volatile-lfu" + return string(naiscrd.ValkeyMaxMemoryPolicyVolatileLFU) case ValkeyMaxMemoryPolicyVolatileLru: - return "volatile-lru" + return string(naiscrd.ValkeyMaxMemoryPolicyVolatileLRU) case ValkeyMaxMemoryPolicyVolatileRandom: - return "volatile-random" + return string(naiscrd.ValkeyMaxMemoryPolicyVolatileRandom) case ValkeyMaxMemoryPolicyVolatileTTL: - return "volatile-ttl" + return string(naiscrd.ValkeyMaxMemoryPolicyVolatileTTL) default: return "" } diff --git a/internal/persistence/valkey/queries.go b/internal/persistence/valkey/queries.go index be46667bc..a21660d91 100644 --- a/internal/persistence/valkey/queries.go +++ b/internal/persistence/valkey/queries.go @@ -2,6 +2,7 @@ package valkey import ( "context" + "errors" "fmt" "regexp" "strconv" @@ -20,17 +21,19 @@ import ( "github.com/nais/api/internal/slug" "github.com/nais/api/internal/thirdparty/aiven" nais_io_v1 "github.com/nais/liberator/pkg/apis/nais.io/v1" + "github.com/nais/pgrator/pkg/api" + naiscrd "github.com/nais/pgrator/pkg/api/v1" k8serrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) var ( - specPlan = []string{"spec", "plan"} specTerminationProtection = []string{"spec", "terminationProtection"} specMaxMemoryPolicy = []string{"spec", "userConfig", "valkey_maxmemory_policy"} specNotifyKeyspaceEvents = []string{"spec", "userConfig", "valkey_notify_keyspace_events"} specNumberOfDatabases = []string{"spec", "userConfig", "valkey_number_of_databases"} + specValkeyPersistence = []string{"spec", "userConfig", "valkey_persistence"} ) func GetByIdent(ctx context.Context, id ident.Ident) (*Valkey, error) { @@ -43,11 +46,15 @@ func GetByIdent(ctx context.Context, id ident.Ident) (*Valkey, error) { } func Get(ctx context.Context, teamSlug slug.Slug, environment, name string) (*Valkey, error) { - prefix := instanceNamer(teamSlug, "") - if !strings.HasPrefix(name, prefix) { - name = instanceNamer(teamSlug, name) + v, err := fromContext(ctx).naisWatcher.Get(environment, teamSlug.String(), name) + if errors.Is(err, &watcher.ErrorNotFound{}) { + prefix := instanceNamer(teamSlug, "") + if !strings.HasPrefix(name, prefix) { + name = instanceNamer(teamSlug, name) + } + v, err = fromContext(ctx).watcher.Get(environment, teamSlug.String(), name) } - return fromContext(ctx).client.watcher.Get(environment, teamSlug.String(), name) + return v, err } func ListForTeam(ctx context.Context, teamSlug slug.Slug, page *pagination.Pagination, orderBy *ValkeyOrder, filter *ValkeyFilter) (*ValkeyConnection, error) { @@ -64,7 +71,9 @@ func ListForTeam(ctx context.Context, teamSlug slug.Slug, page *pagination.Pagin } func ListAllForTeam(ctx context.Context, teamSlug slug.Slug) []*Valkey { - all := fromContext(ctx).client.watcher.GetByNamespace(teamSlug.String(), watcher.WithoutDeleted()) + all := fromContext(ctx).watcher.GetByNamespace(teamSlug.String(), watcher.WithoutDeleted()) + allNais := fromContext(ctx).naisWatcher.GetByNamespace(teamSlug.String(), watcher.WithoutDeleted()) + all = append(all, allNais...) return watcher.Objects(all) } @@ -98,13 +107,13 @@ func ListAccess(ctx context.Context, valkey *Valkey, page *pagination.Pagination } func ListForWorkload(ctx context.Context, teamSlug slug.Slug, environmentName string, references []nais_io_v1.Valkey, orderBy *ValkeyOrder) (*ValkeyConnection, error) { - all := fromContext(ctx).client.watcher.GetByNamespace(teamSlug.String(), watcher.InCluster(environmentName)) + all := ListAllForTeam(ctx, teamSlug) ret := make([]*Valkey, 0) for _, ref := range references { for _, d := range all { - if d.Obj.FullyQualifiedName() == instanceNamer(teamSlug, ref.Instance) { - ret = append(ret, d.Obj) + if d.FullyQualifiedName() == instanceNamer(teamSlug, ref.Instance) || d.FullyQualifiedName() == ref.Instance { + ret = append(ret, d) } } } @@ -130,79 +139,74 @@ func Create(ctx context.Context, input CreateValkeyInput) (*CreateValkeyPayload, return nil, err } - namespace := input.TeamSlug.String() - client, err := fromContext(ctx).watcher.ImpersonatedClientWithNamespace(ctx, input.EnvironmentName, namespace) + client, err := newK8sClient(ctx, input.EnvironmentName, input.TeamSlug) if err != nil { return nil, err } - machine, err := machineTypeFromTierAndMemory(input.Tier, input.Memory) - if err != nil { + // Ensure there's no existing Aiven Valkey with the same name + // This can be removed when we manage all valkeys through Console + _, err = fromContext(ctx).watcher.Get(input.EnvironmentName, input.TeamSlug.String(), instanceNamer(input.TeamSlug, input.Name)) + if err == nil { + return nil, apierror.Errorf("Valkey with the name %q already exists, but are not yet managed through Console.", input.Name) + } else if !errors.Is(err, &watcher.ErrorNotFound{}) { return nil, err } - res := &unstructured.Unstructured{} - res.SetAPIVersion("aiven.io/v1alpha1") - res.SetKind("Valkey") - res.SetName(instanceNamer(input.TeamSlug, input.Name)) - res.SetNamespace(namespace) + res := &naiscrd.Valkey{ + TypeMeta: metav1.TypeMeta{ + Kind: "Valkey", + APIVersion: "nais.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: input.Name, + Namespace: input.TeamSlug.String(), + }, + Spec: naiscrd.ValkeySpec{ + Tier: toMapperatorTier(input.Tier), + Memory: toMapperatorMemory(input.Memory), + }, + } res.SetAnnotations(kubernetes.WithCommonAnnotations(nil, authz.ActorFromContext(ctx).User.Identity())) kubernetes.SetManagedByConsoleLabel(res) - aivenProject, err := aiven.GetProject(ctx, input.EnvironmentName) - if err != nil { - return nil, err + if input.MaxMemoryPolicy != nil { + res.Spec.MaxMemoryPolicy = naiscrd.ValkeyMaxMemoryPolicy(input.MaxMemoryPolicy.ToAivenString()) } - - res.Object["spec"] = map[string]any{ - "cloudName": "google-europe-north1", - "plan": machine.AivenPlan, - "project": aivenProject.ID, - "projectVpcId": aivenProject.VPC, - "terminationProtection": true, - "tags": map[string]any{ - "environment": input.EnvironmentName, - "team": namespace, - "tenant": fromContext(ctx).tenantName, - }, + if input.NotifyKeyspaceEvents != nil { + res.Spec.NotifyKeyspaceEvents = *input.NotifyKeyspaceEvents + } + if input.Databases != nil { + res.Spec.Databases = input.Databases } - if input.MaxMemoryPolicy != nil { - maxMemoryPolicy := input.MaxMemoryPolicy.ToAivenString() - err := unstructured.SetNestedField(res.Object, maxMemoryPolicy, specMaxMemoryPolicy...) - if err != nil { - return nil, err + if input.PersistenceDisabled != nil { + res.Spec.Persistence = &naiscrd.ValkeyPersistence{ + Disabled: *input.PersistenceDisabled, } } - if input.NotifyKeyspaceEvents != nil { - err := unstructured.SetNestedField(res.Object, *input.NotifyKeyspaceEvents, specNotifyKeyspaceEvents...) - if err != nil { - return nil, err - } + obj, err := kubernetes.ToUnstructured(res) + if err != nil { + return nil, err } + // TODO: fix if input.Databases != nil { // Must be stored and read as float64 for the integration tests to work. - err := unstructured.SetNestedField(res.Object, float64(*input.Databases), specNumberOfDatabases...) + err := unstructured.SetNestedField(obj.Object, float64(*input.Databases), "spec", "databases") if err != nil { return nil, err } } - ret, err := client.Create(ctx, res, metav1.CreateOptions{}) - if err != nil { + if _, err = client.Create(ctx, obj, metav1.CreateOptions{}); err != nil { if k8serrors.IsAlreadyExists(err) { return nil, apierror.ErrAlreadyExists } return nil, err } - err = aiven.UpsertPrometheusServiceIntegration(ctx, fromContext(ctx).watcher, ret, aivenProject, input.EnvironmentName) - if err != nil { - return nil, fmt.Errorf("creating Prometheus service integration: %w", err) - } - err = activitylog.Create(ctx, activitylog.CreateInput{ Action: activitylog.ActivityLogEntryActionCreated, Actor: authz.ActorFromContext(ctx).User, @@ -215,7 +219,7 @@ func Create(ctx context.Context, input CreateValkeyInput) (*CreateValkeyPayload, return nil, err } - valkey, err := toValkey(ret, input.EnvironmentName) + valkey, err := toValkeyFromNais(res, input.EnvironmentName) if err != nil { return nil, err } @@ -230,47 +234,55 @@ func Update(ctx context.Context, input UpdateValkeyInput) (*UpdateValkeyPayload, return nil, err } - client, err := fromContext(ctx).watcher.ImpersonatedClientWithNamespace(ctx, input.EnvironmentName, input.TeamSlug.String()) + client, err := newK8sClient(ctx, input.EnvironmentName, input.TeamSlug) if err != nil { return nil, err } - valkey, err := client.Get(ctx, instanceNamer(input.TeamSlug, input.Name), metav1.GetOptions{}) + valkey, err := client.Get(ctx, input.Name, metav1.GetOptions{}) if err != nil { return nil, err } - if !kubernetes.HasManagedByConsoleLabel(valkey) { - return nil, apierror.Errorf("Valkey %s/%s is not managed by Console", input.TeamSlug, input.Name) - } - changes := make([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, 0) - - res, err := updatePlan(valkey, input) + concreteValkey, err := kubernetes.ToConcrete[naiscrd.Valkey](valkey) if err != nil { return nil, err } - changes = append(changes, res...) - res, err = updateMaxMemoryPolicy(valkey, input) - if err != nil { - return nil, err + changes := make([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, 0) + updateFuncs := []func(*naiscrd.Valkey, UpdateValkeyInput) ([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, error){ + updateTier, + updateMemory, + updateMaxMemoryPolicy, + updateNotifyKeyspaceEvents, + updateDatabases, + updatePersistence, } - changes = append(changes, res...) - res, err = updateNotifyKeyspaceEvents(valkey, input) - if err != nil { - return nil, err + for _, f := range updateFuncs { + res, err := f(concreteValkey, input) + if err != nil { + return nil, err + } + changes = append(changes, res...) } - changes = append(changes, res...) - res, err = updateDatabases(valkey, input) - if err != nil { + // TODO: fix + databases := concreteValkey.Spec.Databases + if input.Databases != nil { + databases = input.Databases + } + // Must be stored and read as float64 for the integration tests to work. + if err := unstructured.SetNestedField(valkey.Object, float64(*databases), "spec", "databases"); err != nil { return nil, err } - changes = append(changes, res...) if len(changes) == 0 { - vk, err := toValkey(valkey, input.EnvironmentName) + v, err := kubernetes.ToConcrete[naiscrd.Valkey](valkey) + if err != nil { + return nil, err + } + vk, err := toValkeyFromNais(v, input.EnvironmentName) if err != nil { return nil, err } @@ -280,21 +292,22 @@ func Update(ctx context.Context, input UpdateValkeyInput) (*UpdateValkeyPayload, }, nil } - valkey.SetAnnotations(kubernetes.WithCommonAnnotations(valkey.GetAnnotations(), authz.ActorFromContext(ctx).User.Identity())) - - ret, err := client.Update(ctx, valkey, metav1.UpdateOptions{}) + obj, err := kubernetes.ToUnstructured(concreteValkey) if err != nil { return nil, err } - aivenProject, err := aiven.GetProject(ctx, input.EnvironmentName) - if err != nil { + // TODO: fix + // Must be stored and read as float64 for the integration tests to work. + if err := unstructured.SetNestedField(obj.Object, float64(*databases), "spec", "databases"); err != nil { return nil, err } - err = aiven.UpsertPrometheusServiceIntegration(ctx, fromContext(ctx).watcher, ret, aivenProject, input.EnvironmentName) + obj.SetAnnotations(kubernetes.WithCommonAnnotations(obj.GetAnnotations(), authz.ActorFromContext(ctx).User.Identity())) + + ret, err := client.Update(ctx, obj, metav1.UpdateOptions{}) if err != nil { - return nil, fmt.Errorf("creating Prometheus service integration: %w", err) + return nil, err } err = activitylog.Create(ctx, activitylog.CreateInput{ @@ -312,7 +325,12 @@ func Update(ctx context.Context, input UpdateValkeyInput) (*UpdateValkeyPayload, return nil, err } - valkeyUpdated, err := toValkey(ret, input.EnvironmentName) + retValkey, err := kubernetes.ToConcrete[naiscrd.Valkey](ret) + if err != nil { + return nil, err + } + + valkeyUpdated, err := toValkeyFromNais(retValkey, input.EnvironmentName) if err != nil { return nil, err } @@ -376,158 +394,148 @@ func valkeyEnvVarSuffix(instanceName string) string { return strings.ToUpper(valkeyNamePattern.ReplaceAllString(instanceName, "_")) } -func updatePlan(valkey *unstructured.Unstructured, input UpdateValkeyInput) ([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, error) { +func updateTier(valkey *naiscrd.Valkey, input UpdateValkeyInput) ([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, error) { changes := make([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, 0) - desired, err := machineTypeFromTierAndMemory(input.Tier, input.Memory) - if err != nil { - return nil, err - } - - oldPlan, found, err := unstructured.NestedString(valkey.Object, specPlan...) - if err != nil { - return nil, err - } - if !found { - // .spec.plan is a required field - return nil, fmt.Errorf("missing .spec.plan in Valkey resource") - } - - if oldPlan == desired.AivenPlan { - return changes, nil - } - - oldMachine, err := machineTypeFromPlan(oldPlan) - if err != nil { - return nil, err - } - - if input.Tier != oldMachine.Tier { + origTier := fromMapperatorTier(valkey.Spec.Tier) + if input.Tier != origTier { changes = append(changes, &ValkeyUpdatedActivityLogEntryDataUpdatedField{ Field: "tier", - OldValue: new(oldMachine.Tier.String()), + OldValue: new(origTier.String()), NewValue: new(input.Tier.String()), }) } - if input.Memory != oldMachine.Memory { + valkey.Spec.Tier = toMapperatorTier(input.Tier) + + return changes, nil +} + +func updateMemory(valkey *naiscrd.Valkey, input UpdateValkeyInput) ([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, error) { + changes := make([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, 0) + + origMemory := fromMapperatorMemory(valkey.Spec.Memory) + if input.Memory != origMemory { changes = append(changes, &ValkeyUpdatedActivityLogEntryDataUpdatedField{ Field: "memory", - OldValue: new(oldMachine.Memory.String()), + OldValue: new(origMemory.String()), NewValue: new(input.Memory.String()), }) } - if err := unstructured.SetNestedField(valkey.Object, desired.AivenPlan, specPlan...); err != nil { - return nil, err - } + valkey.Spec.Memory = toMapperatorMemory(input.Memory) return changes, nil } -func updateMaxMemoryPolicy(valkey *unstructured.Unstructured, input UpdateValkeyInput) ([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, error) { - changes := make([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, 0) - +func updateMaxMemoryPolicy(valkey *naiscrd.Valkey, input UpdateValkeyInput) ([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, error) { if input.MaxMemoryPolicy == nil { - return changes, nil + return nil, nil } - oldAivenPolicy, found, err := unstructured.NestedString(valkey.Object, specMaxMemoryPolicy...) - if err != nil { - return nil, err + if string(valkey.Spec.MaxMemoryPolicy) == input.MaxMemoryPolicy.ToAivenString() { + return nil, nil } - if found && oldAivenPolicy == input.MaxMemoryPolicy.ToAivenString() { - return changes, nil - } - // continue if not found so that we explicitly set the policy on the resource - - var oldValue *string - if found { - oldPolicy, err := ValkeyMaxMemoryPolicyFromAivenString(oldAivenPolicy) + var oldMMP *string + if valkey.Spec.MaxMemoryPolicy != "" { + old, err := ValkeyMaxMemoryPolicyFromAivenString(valkey.Spec.MaxMemoryPolicy) if err != nil { - return nil, err + return nil, fmt.Errorf("parsing existing max memory policy: %w", err) } - oldValue = new(oldPolicy.String()) + oldMMP = new(old.String()) } + changes := make([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, 0) + changes = append(changes, &ValkeyUpdatedActivityLogEntryDataUpdatedField{ Field: "maxMemoryPolicy", - OldValue: oldValue, + OldValue: oldMMP, NewValue: new(input.MaxMemoryPolicy.String()), }) - maxMemoryPolicy := input.MaxMemoryPolicy.ToAivenString() - if err := unstructured.SetNestedField(valkey.Object, maxMemoryPolicy, specMaxMemoryPolicy...); err != nil { - return nil, err - } + valkey.Spec.MaxMemoryPolicy = naiscrd.ValkeyMaxMemoryPolicy(input.MaxMemoryPolicy.ToAivenString()) return changes, nil } -func updateNotifyKeyspaceEvents(valkey *unstructured.Unstructured, input UpdateValkeyInput) ([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, error) { - changes := make([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, 0) - +func updateNotifyKeyspaceEvents(valkey *naiscrd.Valkey, input UpdateValkeyInput) ([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, error) { if input.NotifyKeyspaceEvents == nil { - return changes, nil + return nil, nil } - oldValue, found, err := unstructured.NestedString(valkey.Object, specNotifyKeyspaceEvents...) - if err != nil { - return nil, err + if valkey.Spec.NotifyKeyspaceEvents == *input.NotifyKeyspaceEvents { + return nil, nil } - if found && oldValue == *input.NotifyKeyspaceEvents { - return changes, nil - } + changes := make([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, 0) - var oldValPtr *string - if found { - oldValPtr = new(oldValue) + var oldValue *string + if valkey.Spec.NotifyKeyspaceEvents != "" { + oldValue = new(valkey.Spec.NotifyKeyspaceEvents) } - changes = append(changes, &ValkeyUpdatedActivityLogEntryDataUpdatedField{ Field: "notifyKeyspaceEvents", - OldValue: oldValPtr, + OldValue: oldValue, NewValue: input.NotifyKeyspaceEvents, }) - if err := unstructured.SetNestedField(valkey.Object, *input.NotifyKeyspaceEvents, specNotifyKeyspaceEvents...); err != nil { - return nil, err - } - + valkey.Spec.NotifyKeyspaceEvents = *input.NotifyKeyspaceEvents return changes, nil } -func updateDatabases(valkey *unstructured.Unstructured, input UpdateValkeyInput) ([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, error) { +func updateDatabases(valkey *naiscrd.Valkey, input UpdateValkeyInput) ([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, error) { changes := make([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, 0) if input.Databases == nil { return changes, nil } - oldValue, found, _ := unstructured.NestedNumberAsFloat64(valkey.Object, specNumberOfDatabases...) - if found && oldValue == float64(*input.Databases) { + oldValue := valkey.Spec.Databases + if oldValue == input.Databases { return changes, nil } - var oldValPtr *string - if found { - oldValPtr = new(strconv.FormatInt(int64(oldValue), 10)) - } - - newVal := strconv.Itoa(*input.Databases) changes = append(changes, &ValkeyUpdatedActivityLogEntryDataUpdatedField{ - Field: "databases", - OldValue: oldValPtr, - NewValue: &newVal, + Field: "databases", + OldValue: func() *string { + if oldValue != nil { + return new(strconv.FormatInt(int64(*oldValue), 10)) + } + return nil + }(), + NewValue: new(strconv.Itoa(*input.Databases)), }) - // Must be stored and read as float64 for the integration tests to work. - if err := unstructured.SetNestedField(valkey.Object, float64(*input.Databases), specNumberOfDatabases...); err != nil { - return nil, err + valkey.Spec.Databases = input.Databases + return changes, nil +} + +func updatePersistence(valkey *naiscrd.Valkey, input UpdateValkeyInput) ([]*ValkeyUpdatedActivityLogEntryDataUpdatedField, error) { + if input.PersistenceDisabled == nil { + return nil, nil } + oldDisabled := false + if valkey.Spec.Persistence != nil { + oldDisabled = valkey.Spec.Persistence.Disabled + } + + if oldDisabled == *input.PersistenceDisabled { + return nil, nil + } + + changes := []*ValkeyUpdatedActivityLogEntryDataUpdatedField{ + { + Field: "persistenceDisabled", + OldValue: new(strconv.FormatBool(oldDisabled)), + NewValue: new(strconv.FormatBool(*input.PersistenceDisabled)), + }, + } + + valkey.Spec.Persistence = &naiscrd.ValkeyPersistence{ + Disabled: *input.PersistenceDisabled, + } return changes, nil } @@ -536,13 +544,12 @@ func Delete(ctx context.Context, input DeleteValkeyInput) (*DeleteValkeyPayload, return nil, err } - name := instanceNamer(input.TeamSlug, input.Name) - client, err := fromContext(ctx).watcher.ImpersonatedClientWithNamespace(ctx, input.EnvironmentName, input.TeamSlug.String()) + client, err := newK8sClient(ctx, input.EnvironmentName, input.TeamSlug) if err != nil { return nil, err } - valkey, err := client.Get(ctx, name, metav1.GetOptions{}) + valkey, err := client.Get(ctx, input.Name, metav1.GetOptions{}) if err != nil { return nil, err } @@ -551,22 +558,21 @@ func Delete(ctx context.Context, input DeleteValkeyInput) (*DeleteValkeyPayload, return nil, apierror.Errorf("Valkey %s/%s is not managed by Console", input.TeamSlug, input.Name) } - terminationProtection, found, err := unstructured.NestedBool(valkey.Object, specTerminationProtection...) - if err != nil { - return nil, err + annotations := valkey.GetAnnotations() + if annotations == nil { + annotations = make(map[string]string) } - if found && terminationProtection { - if err := unstructured.SetNestedField(valkey.Object, false, specTerminationProtection...); err != nil { - return nil, err - } + if annotations[api.AllowDeletionAnnotation] != "true" { + annotations[api.AllowDeletionAnnotation] = "true" + valkey.SetAnnotations(annotations) _, err = client.Update(ctx, valkey, metav1.UpdateOptions{}) if err != nil { - return nil, fmt.Errorf("removing deletion protection: %w", err) + return nil, fmt.Errorf("set allow deletion annotation: %w", err) } } - if err := fromContext(ctx).watcher.Delete(ctx, input.EnvironmentName, input.TeamSlug.String(), name); err != nil { + if err := client.Delete(ctx, input.Name, metav1.DeleteOptions{}); err != nil { return nil, err } @@ -587,7 +593,13 @@ func Delete(ctx context.Context, input DeleteValkeyInput) (*DeleteValkeyPayload, } func State(ctx context.Context, v *Valkey) (ValkeyState, error) { - s, err := fromContext(ctx).aivenClient.ServiceGet(ctx, v.AivenProject, v.FullyQualifiedName()) + project, err := aiven.GetProject(ctx, v.EnvironmentName) + if err != nil { + return ValkeyStateUnknown, err + } + aivenProject := project.ID + + s, err := fromContext(ctx).aivenClient.ServiceGet(ctx, aivenProject, v.FullyQualifiedName()) if err != nil { // The Valkey instance may not have been created in Aiven yet, or it has been deleted. // In both cases, we return "unknown" state rather than an error. @@ -610,3 +622,71 @@ func State(ctx context.Context, v *Valkey) (ValkeyState, error) { return ValkeyStateUnknown, nil } } + +func toMapperatorTier(tier ValkeyTier) naiscrd.ValkeyTier { + switch tier { + case ValkeyTierSingleNode: + return naiscrd.ValkeyTierSingleNode + case ValkeyTierHighAvailability: + return naiscrd.ValkeyTierHighAvailability + default: + return "" + } +} + +func fromMapperatorTier(tier naiscrd.ValkeyTier) ValkeyTier { + switch tier { + case naiscrd.ValkeyTierSingleNode: + return ValkeyTierSingleNode + case naiscrd.ValkeyTierHighAvailability: + return ValkeyTierHighAvailability + default: + return "" + } +} + +func toMapperatorMemory(memory ValkeyMemory) naiscrd.ValkeyMemory { + switch memory { + case ValkeyMemoryGB1: + return naiscrd.ValkeyMemory1GB + case ValkeyMemoryGB4: + return naiscrd.ValkeyMemory4GB + case ValkeyMemoryGB8: + return naiscrd.ValkeyMemory8GB + case ValkeyMemoryGB14: + return naiscrd.ValkeyMemory14GB + case ValkeyMemoryGB28: + return naiscrd.ValkeyMemory28GB + case ValkeyMemoryGB56: + return naiscrd.ValkeyMemory56GB + case ValkeyMemoryGB112: + return naiscrd.ValkeyMemory112GB + case ValkeyMemoryGB200: + return naiscrd.ValkeyMemory200GB + default: + return "" + } +} + +func fromMapperatorMemory(memory naiscrd.ValkeyMemory) ValkeyMemory { + switch memory { + case naiscrd.ValkeyMemory1GB: + return ValkeyMemoryGB1 + case naiscrd.ValkeyMemory4GB: + return ValkeyMemoryGB4 + case naiscrd.ValkeyMemory8GB: + return ValkeyMemoryGB8 + case naiscrd.ValkeyMemory14GB: + return ValkeyMemoryGB14 + case naiscrd.ValkeyMemory28GB: + return ValkeyMemoryGB28 + case naiscrd.ValkeyMemory56GB: + return ValkeyMemoryGB56 + case naiscrd.ValkeyMemory112GB: + return ValkeyMemoryGB112 + case naiscrd.ValkeyMemory200GB: + return ValkeyMemoryGB200 + default: + return "" + } +} diff --git a/internal/servicemaintenance/queries.go b/internal/servicemaintenance/queries.go index 1af0f5032..3467042b6 100644 --- a/internal/servicemaintenance/queries.go +++ b/internal/servicemaintenance/queries.go @@ -12,6 +12,7 @@ import ( "github.com/nais/api/internal/persistence/opensearch" "github.com/nais/api/internal/persistence/valkey" servicemaintenanceal "github.com/nais/api/internal/servicemaintenance/activitylog" + "github.com/nais/api/internal/thirdparty/aiven" ) func StartValkeyMaintenance(ctx context.Context, input StartValkeyMaintenanceInput) error { @@ -20,7 +21,12 @@ func StartValkeyMaintenance(ctx context.Context, input StartValkeyMaintenanceInp return err } - if err := fromContext(ctx).maintenanceMutator.aivenClient.ServiceMaintenanceStart(ctx, vk.AivenProject, vk.FullyQualifiedName()); err != nil { + project, err := aiven.GetProject(ctx, input.EnvironmentName) + if err != nil { + return err + } + + if err := fromContext(ctx).maintenanceMutator.aivenClient.ServiceMaintenanceStart(ctx, project.ID, vk.FullyQualifiedName()); err != nil { fromContext(ctx).log.WithError(err).Error("Failed to start Valkey maintenance") return err } @@ -41,7 +47,11 @@ func StartOpenSearchMaintenance(ctx context.Context, input StartOpenSearchMainte return err } - if err := fromContext(ctx).maintenanceMutator.aivenClient.ServiceMaintenanceStart(ctx, instance.AivenProject, instance.FullyQualifiedName()); err != nil { + project, err := aiven.GetProject(ctx, input.EnvironmentName) + if err != nil { + return err + } + if err := fromContext(ctx).maintenanceMutator.aivenClient.ServiceMaintenanceStart(ctx, project.ID, instance.FullyQualifiedName()); err != nil { fromContext(ctx).log.WithError(err).Error("Failed to start OpenSearch maintenance") return err }