diff --git a/go.mod b/go.mod index d64228a8..c946f7c8 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,14 @@ go 1.25.7 replace github.com/yudai/gojsondiff v1.0.0 => github.com/Kong/gojsondiff v1.3.0 +replace ( + k8s.io/api => k8s.io/api v0.33.1 + k8s.io/apimachinery => k8s.io/apimachinery v0.33.1 + k8s.io/client-go => k8s.io/client-go v0.33.1 + k8s.io/code-generator => k8s.io/code-generator v0.33.1 + k8s.io/kubectl => k8s.io/kubectl v0.33.1 +) + require ( dario.cat/mergo v1.0.2 github.com/Kong/gojsondiff v1.3.2 @@ -20,8 +28,8 @@ require ( github.com/hashicorp/go-memdb v1.3.5 github.com/hashicorp/go-retryablehttp v0.7.8 github.com/hexops/gotextdiff v1.0.3 - github.com/kong/deck v1.56.0 - github.com/kong/go-kong v0.72.1 + github.com/kong/deck v1.55.1 + github.com/kong/go-kong v0.72.1-0.20260313025716-eb4629dd857b github.com/samber/lo v1.52.0 github.com/shirou/gopsutil/v3 v3.24.5 github.com/ssgelm/cookiejarparser v1.0.1 @@ -127,16 +135,20 @@ require ( golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect golang.org/x/mod v0.29.0 // indirect golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.31.0 // indirect golang.org/x/tools v0.38.0 // indirect google.golang.org/protobuf v1.36.8 // indirect + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.35.1 // indirect + k8s.io/api v0.35.2 // indirect k8s.io/apiextensions-apiserver v0.33.1 // indirect - k8s.io/apimachinery v0.35.1 // indirect + k8s.io/apimachinery v0.35.2 // indirect + k8s.io/client-go v0.35.2 // indirect + k8s.io/component-helpers v0.35.2 // indirect k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b // indirect k8s.io/klog/v2 v2.130.1 // indirect k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect @@ -145,5 +157,6 @@ require ( sigs.k8s.io/gateway-api v1.3.0 // 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/v4 v4.7.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect ) diff --git a/go.sum b/go.sum index b42b54a8..36f2803b 100644 --- a/go.sum +++ b/go.sum @@ -27,9 +27,6 @@ github.com/MarvinJWendt/testza v0.3.0/go.mod h1:eFcL4I0idjtIx8P9C6KkAuLgATNKpX4/ github.com/MarvinJWendt/testza v0.4.2/go.mod h1:mSdhXiKH8sg/gQehJ63bINcCKp7RtYewEjXsvsVUPbE= github.com/MarvinJWendt/testza v0.5.2 h1:53KDo64C1z/h/d/stCYCPY69bt/OSwjq5KpFNwi+zB4= github.com/MarvinJWendt/testza v0.5.2/go.mod h1:xu53QFE5sCdjtMCKk8YMQ2MnymimEctc4n3EjyIYvEY= -github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= -github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= -github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= @@ -221,12 +218,12 @@ github.com/klauspost/cpuid/v2 v2.0.10/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuOb github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= -github.com/kong/deck v1.56.0 h1:HpVpODc3Ct0X+3GnZZlcuTK2zwxAioNK8IlVjX0XbU0= -github.com/kong/deck v1.56.0/go.mod h1:0qVMay6jN0fc8hlpKcS+ggiLCnXwbMH1gBXg87ueRB4= +github.com/kong/deck v1.55.1 h1:p7Mg6ruIsrpOWPigvKOv8GdPznnTy7/CYryTlnIm4W8= +github.com/kong/deck v1.55.1/go.mod h1:wTzQPElIoKgBheytGTyzY+wlHAWd72hUDFDeUmOa2V4= github.com/kong/go-apiops v0.2.2 h1:Owdcl/PxTdtciqyZKgPScVhTKHgY2b8dGIC1Bms8NpI= github.com/kong/go-apiops v0.2.2/go.mod h1:yPwbl3P2eQinVGAEA0d3legaYmzPJ+WtJf9fSeGF4b8= -github.com/kong/go-kong v0.72.1 h1:rQ69f3Wd0Fvc3JANkavo34vePqR4uZG/YQ2y5U7d2Po= -github.com/kong/go-kong v0.72.1/go.mod h1:J0vGB3wsZ2i99zly1zTRe3v7rOKpkhQZRwbcTFP76qM= +github.com/kong/go-kong v0.72.1-0.20260313025716-eb4629dd857b h1:OmGL1ujx54UH6Jqth/RGR6vSc2jZBPkabIrX7EpczLU= +github.com/kong/go-kong v0.72.1-0.20260313025716-eb4629dd857b/go.mod h1:J0vGB3wsZ2i99zly1zTRe3v7rOKpkhQZRwbcTFP76qM= github.com/kong/go-slugify v1.0.0 h1:vCFAyf2sdoSlBtLcrmDWUFn0ohlpKiKvQfXZkO5vSKY= github.com/kong/go-slugify v1.0.0/go.mod h1:dbR2h3J2QKXQ1k0aww6cN7o4cIcwlWflr6RKRdcoaiw= github.com/kong/kubernetes-configuration v1.4.2 h1:/OafLbl2NucvgQV7Xf/uneIgjxmPPUeE92BrssfVAQY= @@ -296,15 +293,15 @@ github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vv github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= -github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= -github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= +github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU= +github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= -github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= -github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= +github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8= +github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= @@ -339,8 +336,8 @@ github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= -github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= +github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= @@ -544,8 +541,8 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= -golang.org/x/tools/go/expect v0.1.1-deprecated h1:jpBZDwmgPhXsKZC6WhL20P4b/wmnpsEAGHaNy0n/rJM= -golang.org/x/tools/go/expect v0.1.1-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= +golang.org/x/tools/go/expect v0.1.0-deprecated h1:jY2C5HGYR5lqex3gEniOQL0r7Dq5+VGVgY1nudX5lXY= +golang.org/x/tools/go/expect v0.1.0-deprecated/go.mod h1:eihoPOH+FgIqa3FpoTwguz/bVUSGBlGQU67vpBeOrBY= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated h1:1h2MnaIAIXISqTFKdENegdpAgUXz6NrPEsbIeWaBRvM= golang.org/x/tools/go/packages/packagestest v0.1.1-deprecated/go.mod h1:RVAQXBGNv1ib0J382/DPCRS/BPnsGebyM1Gj5VSDpG8= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -587,28 +584,30 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.35.1 h1:0PO/1FhlK/EQNVK5+txc4FuhQibV25VLSdLMmGpDE/Q= -k8s.io/api v0.35.1/go.mod h1:28uR9xlXWml9eT0uaGo6y71xK86JBELShLy4wR1XtxM= +k8s.io/api v0.33.1 h1:tA6Cf3bHnLIrUK4IqEgb2v++/GYUtqiu9sRVk3iBXyw= +k8s.io/api v0.33.1/go.mod h1:87esjTn9DRSRTD4fWMXamiXxJhpOIREjWOSjsW1kEHw= k8s.io/apiextensions-apiserver v0.33.1 h1:N7ccbSlRN6I2QBcXevB73PixX2dQNIW0ZRuguEE91zI= k8s.io/apiextensions-apiserver v0.33.1/go.mod h1:uNQ52z1A1Gu75QSa+pFK5bcXc4hq7lpOXbweZgi4dqA= -k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= -k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apimachinery v0.33.1 h1:mzqXWV8tW9Rw4VeW9rEkqvnxj59k1ezDUl20tFK/oM4= +k8s.io/apimachinery v0.33.1/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/cli-runtime v0.31.0 h1:V2Q1gj1u3/WfhD475HBQrIYsoryg/LrhhK4RwpN+DhA= k8s.io/cli-runtime v0.31.0/go.mod h1:vg3H94wsubuvWfSmStDbekvbla5vFGC+zLWqcf+bGDw= -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/code-generator v0.35.1 h1:yLKR2la7Z9cWT5qmk67ayx8xXLM4RRKQMnC8YPvTWRI= -k8s.io/code-generator v0.35.1/go.mod h1:F2Fhm7aA69tC/VkMXLDokdovltXEF026Tb9yfQXQWKg= +k8s.io/client-go v0.33.1 h1:ZZV/Ks2g92cyxWkRRnfUDsnhNn28eFpt26aGc8KbXF4= +k8s.io/client-go v0.33.1/go.mod h1:JAsUrl1ArO7uRVFWfcj6kOomSlCv+JpvIsp6usAGefA= +k8s.io/code-generator v0.33.1 h1:ZLzIRdMsh3Myfnx9BaooX6iQry29UJjVfVG+BuS+UMw= +k8s.io/code-generator v0.33.1/go.mod h1:HUKT7Ubp6bOgIbbaPIs9lpd2Q02uqkMCMx9/GjDrWpY= k8s.io/component-base v0.33.1 h1:EoJ0xA+wr77T+G8p6T3l4efT2oNwbqBVKR71E0tBIaI= k8s.io/component-base v0.33.1/go.mod h1:guT/w/6piyPfTgq7gfvgetyXMIh10zuXA6cRRm3rDuY= +k8s.io/component-helpers v0.35.2 h1:7Ea4CDgHnyOGrl3ZhD8e46SdTyf1itTONnreJ2Q52UM= +k8s.io/component-helpers v0.35.2/go.mod h1:ybIoc8i92FG7xJFrBcEMzB8ul1wlZgfF0I4Z9w0V6VQ= k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b h1:gMplByicHV/TJBizHd9aVEsTYoJBnnUAT5MHlTkbjhQ= k8s.io/gengo/v2 v2.0.0-20250922181213-ec3ebc5fd46b/go.mod h1:CgujABENc3KuTrcsdpGmrrASjtQsWCT7R99mEV4U/fM= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= -k8s.io/kubectl v0.31.0 h1:kANwAAPVY02r4U4jARP/C+Q1sssCcN/1p9Nk+7BQKVg= -k8s.io/kubectl v0.31.0/go.mod h1:pB47hhFypGsaHAPjlwrNbvhXgmuAr01ZBvAIIUaI8d4= +k8s.io/kubectl v0.33.1 h1:OJUXa6FV5bap6iRy345ezEjU9dTLxqv1zFTVqmeHb6A= +k8s.io/kubectl v0.33.1/go.mod h1:Z07pGqXoP4NgITlPRrnmiM3qnoo1QrK1zjw85Aiz8J0= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/controller-runtime v0.20.4 h1:X3c+Odnxz+iPTRobG4tp092+CvBU9UK0t/bRf+n0DGU= @@ -623,9 +622,13 @@ sigs.k8s.io/kustomize/api v0.17.3 h1:6GCuHSsxq7fN5yhF2XrC+AAr8gxQwhexgHflOAD/JJU sigs.k8s.io/kustomize/api v0.17.3/go.mod h1:TuDH4mdx7jTfK61SQ/j1QZM/QWR+5rmEiNjvYlhzFhc= sigs.k8s.io/kustomize/kyaml v0.17.2 h1:+AzvoJUY0kq4QAhH/ydPHHMRLijtUKiyVyh7fOSshr0= sigs.k8s.io/kustomize/kyaml v0.17.2/go.mod h1:9V0mCjIEYjlXuCdYsSXvyoy2BTsLESH7TlGV81S282U= +sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0 h1:qPeWmscJcXP0snki5IYF79Z8xrl8ETFxgMd7wez1XkI= +sigs.k8s.io/structured-merge-diff/v4 v4.7.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= +sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= 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/pkg/diff/diff.go b/pkg/diff/diff.go index 2412e182..bf94061f 100644 --- a/pkg/diff/diff.go +++ b/pkg/diff/diff.go @@ -298,6 +298,8 @@ func (sc *Syncer) init() error { types.DegraphqlRoute, + types.GraphqlRateLimitingCostDecoration, + types.Partial, types.Key, types.KeySet, diff --git a/pkg/diff/order.go b/pkg/diff/order.go index 4f39f737..af119a61 100644 --- a/pkg/diff/order.go +++ b/pkg/diff/order.go @@ -25,6 +25,7 @@ L3 +---------------------------> Service <---+ +-> Route | L4 +----------> Document <---------+ +-> Plugins / <---------+ FilterChains CustomEntities - DegraphqlRoute + - GraphqlRateLimitingCostDecoration */ // dependencyOrder defines the order in which entities will be synced by decK. @@ -69,6 +70,7 @@ var dependencyOrder = [][]types.EntityType{ types.FilterChain, types.Document, types.DegraphqlRoute, + types.GraphqlRateLimitingCostDecoration, }, } diff --git a/pkg/dump/dump.go b/pkg/dump/dump.go index c4ac23aa..c2814743 100644 --- a/pkg/dump/dump.go +++ b/pkg/dump/dump.go @@ -644,19 +644,22 @@ func getProxyConfiguration(ctx context.Context, group *errgroup.Group, } if !skipCustomEntities && len(config.CustomEntityTypes) > 0 { - // Get custom entities with types given in config.CustomEntityTypes. + // Register all entity types first (sequentially) to avoid data race on the registry map. + // The registry is not thread-safe, so we must complete all registrations before + // starting concurrent fetch operations that call Lookup(). + for _, entityType := range config.CustomEntityTypes { + if err := tryRegisterEntityType(client, custom.Type(entityType)); err != nil { + group.Go(func() error { + return fmt.Errorf("custom entity %s: %w", entityType, err) + }) + continue + } + } + customEntityLock := sync.Mutex{} for _, entityType := range config.CustomEntityTypes { t := entityType group.Go(func() error { - // Register entity type. - // Because client writes an unprotected map to register entity types, we need to use mutex to protect it. - customEntityLock.Lock() - err := tryRegisterEntityType(client, custom.Type(t)) - customEntityLock.Unlock() - if err != nil { - return fmt.Errorf("custom entity %s: %w", t, err) - } // Fetch all entities with the given type. entities, err := GetAllCustomEntitiesWithType(ctx, client, t) if err != nil { @@ -676,9 +679,19 @@ func tryRegisterEntityType(client *kong.Client, typ custom.Type) error { if client.Lookup(typ) != nil { return nil } + + // Determine the CRUD path based on entity type + crudPath := "/" + string(typ) + + // Special case: Kong exposes this API at /graphql-rate-limiting-advanced/costs, + // not at /graphql_ratelimiting_cost_decorations (the entity type name). + if typ == "graphql_ratelimiting_cost_decorations" { + crudPath = "/graphql-rate-limiting-advanced/costs" + } + return client.Register(typ, &custom.EntityCRUDDefinition{ Name: typ, - CRUDPath: "/" + string(typ), + CRUDPath: crudPath, PrimaryKey: "id", }) } diff --git a/pkg/file/builder.go b/pkg/file/builder.go index c990eb7d..e7851032 100644 --- a/pkg/file/builder.go +++ b/pkg/file/builder.go @@ -2135,7 +2135,8 @@ func (b *stateBuilder) customEntities() { } supportedCustomEntities := map[string]bool{ - degraphqlRoutesType: true, + degraphqlRoutesType: true, + graphqlRateLimitingCostDecorationsType: true, } var customEntities []FCustomEntity @@ -2156,6 +2157,8 @@ func (b *stateBuilder) ingestCustomEntities(customEntities []FCustomEntity) { switch *e.Type { case degraphqlRoutesType: b.ingestDeGraphqlRoute(e) + case graphqlRateLimitingCostDecorationsType: + b.ingestGraphqlRateLimitingCostDecoration(e) } } } @@ -2258,6 +2261,120 @@ func (b *stateBuilder) copyToDegraphqlRoute(fcEntity FCustomEntity) (DegraphqlRo return degraphqlRoute, nil } +func (b *stateBuilder) ingestGraphqlRateLimitingCostDecoration(entity FCustomEntity) { + decoration, err := b.copyToGraphqlRateLimitingCostDecoration(entity) + if err != nil { + b.err = err + return + } + + if utils.Empty(decoration.ID) { + // Try to find existing by TypePath + d, err := b.currentState.GraphqlRateLimitingCostDecorations.GetByTypePath(*decoration.TypePath) + if errors.Is(err, state.ErrNotFound) { + decoration.ID = uuid() + } else if err != nil { + b.err = err + return + } else { + decoration.ID = kong.String(*d.ID) + } + } else { + decoration.ID = kong.String(*decoration.ID) + } + + b.rawState.GraphqlRateLimitingCostDecorations = append( + b.rawState.GraphqlRateLimitingCostDecorations, + &decoration.GraphqlRateLimitingCostDecoration, + ) +} + +func (b *stateBuilder) copyToGraphqlRateLimitingCostDecoration( + fcEntity FCustomEntity, +) (GraphqlRateLimitingCostDecoration, error) { + decoration := GraphqlRateLimitingCostDecoration{} + + if fcEntity.ID != nil { + decoration.ID = fcEntity.ID + } + + if fcEntity.Fields == nil { + return GraphqlRateLimitingCostDecoration{}, + fmt.Errorf("fields are required for graphql_ratelimiting_cost_decorations") + } + + if fcEntity.Fields["id"] != nil { + if id, ok := fcEntity.Fields["id"].(*string); ok { + decoration.ID = id + } else if id, ok := fcEntity.Fields["id"].(string); ok { + decoration.ID = kong.String(id) + } + } + + if fcEntity.Fields["type_path"] != nil { + if tp, ok := fcEntity.Fields["type_path"].(*string); ok { + decoration.TypePath = tp + } else if tp, ok := fcEntity.Fields["type_path"].(string); ok { + decoration.TypePath = kong.String(tp) + } + } + + if fcEntity.Fields["add_constant"] != nil { + if ac, ok := fcEntity.Fields["add_constant"].(*float64); ok { + decoration.AddConstant = ac + } else if ac, ok := fcEntity.Fields["add_constant"].(float64); ok { + decoration.AddConstant = kong.Float64(ac) + } + } + + if fcEntity.Fields["mul_constant"] != nil { + if mc, ok := fcEntity.Fields["mul_constant"].(*float64); ok { + decoration.MulConstant = mc + } else if mc, ok := fcEntity.Fields["mul_constant"].(float64); ok { + decoration.MulConstant = kong.Float64(mc) + } + } + + if fcEntity.Fields["add_arguments"] != nil { + if args, ok := fcEntity.Fields["add_arguments"].([]*string); ok { + decoration.AddArguments = args + } else if args, ok := fcEntity.Fields["add_arguments"].([]interface{}); ok { + addArgs := make([]*string, len(args)) + for i, arg := range args { + if argStr, ok := arg.(string); ok { + addArgs[i] = kong.String(argStr) + } else if argPtr, ok := arg.(*string); ok { + addArgs[i] = argPtr + } + } + decoration.AddArguments = addArgs + } + } + + if fcEntity.Fields["mul_arguments"] != nil { + if args, ok := fcEntity.Fields["mul_arguments"].([]*string); ok { + decoration.MulArguments = args + } else if args, ok := fcEntity.Fields["mul_arguments"].([]interface{}); ok { + mulArgs := make([]*string, len(args)) + for i, arg := range args { + if argStr, ok := arg.(string); ok { + mulArgs[i] = kong.String(argStr) + } else if argPtr, ok := arg.(*string); ok { + mulArgs[i] = argPtr + } + } + decoration.MulArguments = mulArgs + } + } + + if decoration.TypePath == nil { + return GraphqlRateLimitingCostDecoration{}, + fmt.Errorf("type_path is required for graphql_ratelimiting_cost_decorations") + } + + return decoration, nil +} + func defaulter( ctx context.Context, client *kong.Client, fileContent *Content, disableDynamicDefaults, isKonnect bool, ) (*utils.Defaulter, error) { diff --git a/pkg/file/types.go b/pkg/file/types.go index 235ae080..470302a2 100644 --- a/pkg/file/types.go +++ b/pkg/file/types.go @@ -32,7 +32,8 @@ const ( ) const ( - degraphqlRoutesType = "degraphql_routes" + degraphqlRoutesType = "degraphql_routes" + graphqlRateLimitingCostDecorationsType = "graphql_ratelimiting_cost_decorations" ) // FFilterChain represents a Kong FilterChain. @@ -966,6 +967,13 @@ func (f *FCustomEntity) UnmarshalJSON(b []byte) error { return err } return copyToFCustomEntity(entity, f) + case graphqlRateLimitingCostDecorationsType: + var entity map[string]interface{} + err := json.Unmarshal(b, &entity) + if err != nil { + return err + } + return copyToGraphqlRateLimitingCostDecorationEntity(entity, f) default: return fmt.Errorf("unknown entity type: %s", temp["type"]) } @@ -981,6 +989,12 @@ func (f *FCustomEntity) UnmarshalYAML(unmarshal func(interface{}) error) error { return err } return copyFromDegraphqlRoute(entity, f) + case graphqlRateLimitingCostDecorationsType: + var entity GraphqlRateLimitingCostDecoration + if err := unmarshal(&entity); err != nil { + return err + } + return copyFromGraphqlRateLimitingCostDecoration(entity, f) default: return fmt.Errorf("unknown entity type: %s", *f.Type) } @@ -999,6 +1013,19 @@ type DegraphqlRoute struct { kong.DegraphqlRoute } +// +k8s:deepcopy-gen=true +type GraphqlRateLimitingCostDecoration struct { + kong.GraphqlRateLimitingCostDecoration +} + +// sortKey is used for sorting. +func (g GraphqlRateLimitingCostDecoration) sortKey() string { + if g.ID != nil { + return *g.ID + } + return "" +} + func copyFromDegraphqlRoute(dRoute DegraphqlRoute, fcEntity *FCustomEntity) error { fcEntity.Type = kong.String(degraphqlRoutesType) @@ -1096,6 +1123,123 @@ func (d DegraphqlRoute) sortKey() string { return "" } +func copyFromGraphqlRateLimitingCostDecoration(g GraphqlRateLimitingCostDecoration, fcEntity *FCustomEntity) error { + fcEntity.Type = kong.String(graphqlRateLimitingCostDecorationsType) + + if g.ID != nil { + fcEntity.ID = g.ID + } + + fcEntity.Fields = make(map[string]interface{}) + + if g.TypePath != nil { + fcEntity.Fields["type_path"] = *g.TypePath + } + + if g.AddConstant != nil { + fcEntity.Fields["add_constant"] = *g.AddConstant + } + + if g.MulConstant != nil { + fcEntity.Fields["mul_constant"] = *g.MulConstant + } + + if g.AddArguments != nil { + addArgs := make([]interface{}, len(g.AddArguments)) + for i, arg := range g.AddArguments { + if arg != nil { + addArgs[i] = *arg + } + } + fcEntity.Fields["add_arguments"] = addArgs + } + + if g.MulArguments != nil { + mulArgs := make([]interface{}, len(g.MulArguments)) + for i, arg := range g.MulArguments { + if arg != nil { + mulArgs[i] = *arg + } + } + fcEntity.Fields["mul_arguments"] = mulArgs + } + + return nil +} + +func copyToGraphqlRateLimitingCostDecorationEntity(data map[string]interface{}, fcEntity *FCustomEntity) error { + fcEntity.Type = kong.String(graphqlRateLimitingCostDecorationsType) + + if data["id"] != nil { + fcEntity.ID = kong.String(data["id"].(string)) + } + + fcEntity.Fields = make(map[string]interface{}) + + f, ok := data["fields"].(map[string]interface{}) + if !ok { + return fmt.Errorf("fields field should be a map") + } + + if f["type_path"] != nil { + typePath, ok := f["type_path"].(string) + if !ok { + return fmt.Errorf("type_path field should be a string") + } + fcEntity.Fields["type_path"] = kong.String(typePath) + } + + if f["add_constant"] != nil { + addConstant, ok := f["add_constant"].(float64) + if !ok { + return fmt.Errorf("add_constant field should be a number") + } + fcEntity.Fields["add_constant"] = kong.Float64(addConstant) + } + + if f["mul_constant"] != nil { + mulConstant, ok := f["mul_constant"].(float64) + if !ok { + return fmt.Errorf("mul_constant field should be a number") + } + fcEntity.Fields["mul_constant"] = kong.Float64(mulConstant) + } + + if f["add_arguments"] != nil { + argsArray, ok := f["add_arguments"].([]interface{}) + if !ok { + return fmt.Errorf("add_arguments field should be an array") + } + args := make([]*string, len(argsArray)) + for i, arg := range argsArray { + argStr, ok := arg.(string) + if !ok { + return fmt.Errorf("add_arguments elements should be strings") + } + args[i] = kong.String(argStr) + } + fcEntity.Fields["add_arguments"] = args + } + + if f["mul_arguments"] != nil { + argsArray, ok := f["mul_arguments"].([]interface{}) + if !ok { + return fmt.Errorf("mul_arguments field should be an array") + } + args := make([]*string, len(argsArray)) + for i, arg := range argsArray { + argStr, ok := arg.(string) + if !ok { + return fmt.Errorf("mul_arguments elements should be strings") + } + args[i] = kong.String(argStr) + } + fcEntity.Fields["mul_arguments"] = args + } + + return nil +} + // +k8s:deepcopy-gen=true type FPartial struct { kong.Partial `yaml:",inline,omitempty"` diff --git a/pkg/file/writer.go b/pkg/file/writer.go index eb34dc8d..a9b271ec 100644 --- a/pkg/file/writer.go +++ b/pkg/file/writer.go @@ -208,6 +208,11 @@ func KongStateToContent(kongState *state.KongState, config WriteConfig) (*Conten return nil, err } + err = populateGraphqlRateLimitingCostDecorations(kongState, file) + if err != nil { + return nil, err + } + err = populatePartials(kongState, file) if err != nil { return nil, err @@ -1037,6 +1042,32 @@ func populateDegraphqlRoutes(kongState *state.KongState, file *Content) error { return nil } +func populateGraphqlRateLimitingCostDecorations(kongState *state.KongState, file *Content) error { + decorations, err := kongState.GraphqlRateLimitingCostDecorations.GetAll() + if err != nil { + return err + } + + for _, d := range decorations { + f := FCustomEntity{} + + err := copyFromGraphqlRateLimitingCostDecoration(GraphqlRateLimitingCostDecoration{ + GraphqlRateLimitingCostDecoration: d.GraphqlRateLimitingCostDecoration, + }, &f) + if err != nil { + return err + } + utils.ZeroOutTimestamps(&f) + + file.CustomEntities = append(file.CustomEntities, f) + } + sort.SliceStable(file.CustomEntities, func(i, j int) bool { + return compareOrder(file.CustomEntities[i], file.CustomEntities[j]) + }) + + return nil +} + func populatePartials(kongState *state.KongState, file *Content) error { partials, err := kongState.Partials.GetAll() if err != nil { diff --git a/pkg/state/builder.go b/pkg/state/builder.go index 414690aa..2b7f6ccb 100644 --- a/pkg/state/builder.go +++ b/pkg/state/builder.go @@ -431,8 +431,17 @@ func buildKong(kongState *KongState, raw *utils.KongRawState) error { } } + for _, g := range raw.GraphqlRateLimitingCostDecorations { + err := kongState.GraphqlRateLimitingCostDecorations.Add( + GraphqlRateLimitingCostDecoration{GraphqlRateLimitingCostDecoration: *g}) + if err != nil { + return fmt.Errorf("inserting graphql ratelimiting cost decoration into state: %w", err) + } + } + for _, c := range raw.CustomEntities { - if c.Type() == "degraphql_routes" { + switch c.Type() { + case "degraphql_routes": entity := c.Object() degraphqlRoute, err := buildDegraphqlRouteFromCustomEntity(kongState, entity) @@ -444,6 +453,18 @@ func buildKong(kongState *KongState, raw *utils.KongRawState) error { if err != nil { return fmt.Errorf("inserting degraphql route into state: %w", err) } + case "graphql_ratelimiting_cost_decorations": + entity := c.Object() + + decoration, err := buildGraphqlRateLimitingCostDecorationFromCustomEntity(entity) + if err != nil { + return fmt.Errorf("building graphql ratelimiting cost decoration from custom entity: %w", err) + } + + err = kongState.GraphqlRateLimitingCostDecorations.Add(decoration) + if err != nil { + return fmt.Errorf("inserting graphql ratelimiting cost decoration into state: %w", err) + } } } @@ -585,3 +606,74 @@ func buildDegraphqlRouteFromCustomEntity(kongState *KongState, entity map[string return degraphqlRoute, nil } + +func buildGraphqlRateLimitingCostDecorationFromCustomEntity(entity map[string]interface{}, +) (GraphqlRateLimitingCostDecoration, error) { + var decoration GraphqlRateLimitingCostDecoration + + if entity["id"] != nil { + id, ok := entity["id"].(string) + if !ok { + return GraphqlRateLimitingCostDecoration{}, fmt.Errorf("id must be of type string") + } + decoration.ID = kong.String(id) + } + + if entity["type_path"] != nil { + typePath, ok := entity["type_path"].(string) + if !ok { + return GraphqlRateLimitingCostDecoration{}, fmt.Errorf("type_path must be of type string") + } + decoration.TypePath = kong.String(typePath) + } + + if entity["add_constant"] != nil { + addConstant, ok := entity["add_constant"].(float64) + if !ok { + return GraphqlRateLimitingCostDecoration{}, fmt.Errorf("add_constant must be of type float64") + } + decoration.AddConstant = kong.Float64(addConstant) + } + + if entity["mul_constant"] != nil { + mulConstant, ok := entity["mul_constant"].(float64) + if !ok { + return GraphqlRateLimitingCostDecoration{}, fmt.Errorf("mul_constant must be of type float64") + } + decoration.MulConstant = kong.Float64(mulConstant) + } + + if entity["add_arguments"] != nil { + addArgsSlice, ok := entity["add_arguments"].([]interface{}) + if !ok { + return GraphqlRateLimitingCostDecoration{}, fmt.Errorf("add_arguments must be an array of strings") + } + addArgs := make([]*string, len(addArgsSlice)) + for i, v := range addArgsSlice { + arg, ok := v.(string) + if !ok { + return GraphqlRateLimitingCostDecoration{}, fmt.Errorf("add_arguments must be an array of strings") + } + addArgs[i] = kong.String(arg) + } + decoration.AddArguments = addArgs + } + + if entity["mul_arguments"] != nil { + mulArgsSlice, ok := entity["mul_arguments"].([]interface{}) + if !ok { + return GraphqlRateLimitingCostDecoration{}, fmt.Errorf("mul_arguments must be an array of strings") + } + mulArgs := make([]*string, len(mulArgsSlice)) + for i, v := range mulArgsSlice { + arg, ok := v.(string) + if !ok { + return GraphqlRateLimitingCostDecoration{}, fmt.Errorf("mul_arguments must be an array of strings") + } + mulArgs[i] = kong.String(arg) + } + decoration.MulArguments = mulArgs + } + + return decoration, nil +} diff --git a/pkg/state/graphql_ratelimiting_cost_decoration.go b/pkg/state/graphql_ratelimiting_cost_decoration.go new file mode 100644 index 00000000..18746c6f --- /dev/null +++ b/pkg/state/graphql_ratelimiting_cost_decoration.go @@ -0,0 +1,112 @@ +package state + +import ( + "fmt" + + memdb "github.com/hashicorp/go-memdb" +) + +const graphqlRateLimitingCostDecorationEntityType = "graphql_ratelimiting_cost_decorations" + +// GraphqlRateLimitingCostDecorationsCollection stores and indexes graphql_ratelimiting_cost_decorations. +type GraphqlRateLimitingCostDecorationsCollection struct { + customEntitiesCollection +} + +func newGraphqlRateLimitingCostDecorationsCollection(common collection) *GraphqlRateLimitingCostDecorationsCollection { + return &GraphqlRateLimitingCostDecorationsCollection{ + customEntitiesCollection: customEntitiesCollection{ + collection: common, + CustomEntityType: graphqlRateLimitingCostDecorationEntityType, + customIndexes: map[string]*memdb.IndexSchema{ + "typePath": { + Name: "typePath", + Unique: true, + Indexer: &memdb.StringFieldIndex{Field: "TypePath"}, + }, + }, + }, + } +} + +func getGraphqlRateLimitingCostDecorationByTypePath(txn *memdb.Txn, + typePath string, +) (*GraphqlRateLimitingCostDecoration, error) { + res, err := txn.First(graphqlRateLimitingCostDecorationEntityType, "typePath", typePath) + if err != nil { + return nil, err + } + if res == nil { + return nil, ErrNotFound + } + + g, ok := res.(*GraphqlRateLimitingCostDecoration) + if !ok { + panic(unexpectedType) + } + return &GraphqlRateLimitingCostDecoration{GraphqlRateLimitingCostDecoration: *g.DeepCopy()}, nil +} + +// GetByTypePath gets a graphql ratelimiting cost decoration with +// the same type_path from the collection. +func (k *GraphqlRateLimitingCostDecorationsCollection) GetByTypePath( + typePath string, +) (*GraphqlRateLimitingCostDecoration, error) { + if typePath == "" { + return nil, fmt.Errorf("typePath cannot be empty string") + } + + txn := k.db.Txn(false) + defer txn.Abort() + + return getGraphqlRateLimitingCostDecorationByTypePath(txn, typePath) +} + +// Add adds a graphql ratelimiting cost decoration to the collection +func (k *GraphqlRateLimitingCostDecorationsCollection) Add(decoration GraphqlRateLimitingCostDecoration) error { + e := (customEntity)(&decoration) + return k.customEntitiesCollection.Add(e) +} + +// Get gets a graphql ratelimiting cost decoration by ID. +func (k *GraphqlRateLimitingCostDecorationsCollection) Get(id string) (*GraphqlRateLimitingCostDecoration, error) { + e, err := k.customEntitiesCollection.Get(id) + if err != nil { + return nil, err + } + + decoration, ok := e.(*GraphqlRateLimitingCostDecoration) + if !ok { + panic(unexpectedType) + } + return &GraphqlRateLimitingCostDecoration{GraphqlRateLimitingCostDecoration: *decoration.DeepCopy()}, nil +} + +// Update updates an existing graphql ratelimiting cost decoration +func (k *GraphqlRateLimitingCostDecorationsCollection) Update(decoration GraphqlRateLimitingCostDecoration) error { + e := (customEntity)(&decoration) + return k.customEntitiesCollection.Update(e) +} + +// Delete deletes a graphql ratelimiting cost decoration by ID. +func (k *GraphqlRateLimitingCostDecorationsCollection) Delete(id string) error { + return k.customEntitiesCollection.Delete(id) +} + +// GetAll gets all graphql ratelimiting cost decorations +func (k *GraphqlRateLimitingCostDecorationsCollection) GetAll() ([]*GraphqlRateLimitingCostDecoration, error) { + customEntities, err := k.customEntitiesCollection.GetAll() + if err != nil { + return nil, err + } + + var res []*GraphqlRateLimitingCostDecoration + for _, e := range customEntities { + r, ok := e.(*GraphqlRateLimitingCostDecoration) + if !ok { + panic(unexpectedType) + } + res = append(res, &GraphqlRateLimitingCostDecoration{GraphqlRateLimitingCostDecoration: *r.DeepCopy()}) + } + return res, nil +} diff --git a/pkg/state/graphql_ratelimiting_cost_decoration_test.go b/pkg/state/graphql_ratelimiting_cost_decoration_test.go new file mode 100644 index 00000000..884c23ce --- /dev/null +++ b/pkg/state/graphql_ratelimiting_cost_decoration_test.go @@ -0,0 +1,282 @@ +package state + +import ( + "testing" + + "github.com/kong/go-kong/kong" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func graphqlRateLimitingCostDecorationsCollection() *GraphqlRateLimitingCostDecorationsCollection { + return state().GraphqlRateLimitingCostDecorations +} + +func TestGraphqlRateLimitingCostDecorationAdd(t *testing.T) { + collection := graphqlRateLimitingCostDecorationsCollection() + + tests := []struct { + name string + decoration GraphqlRateLimitingCostDecoration + wantErr bool + }{ + { + name: "adds a decoration to the collection", + decoration: GraphqlRateLimitingCostDecoration{ + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("first"), + TypePath: kong.String("Query.users"), + AddConstant: kong.Float64(1.0), + }, + }, + wantErr: false, + }, + { + name: "adds a decoration with all fields to the collection", + decoration: GraphqlRateLimitingCostDecoration{ + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("second"), + TypePath: kong.String("Query.posts"), + AddConstant: kong.Float64(2.0), + MulConstant: kong.Float64(1.5), + AddArguments: kong.StringSlice("limit"), + MulArguments: kong.StringSlice("first", "last"), + }, + }, + wantErr: false, + }, + { + name: "returns an error when the decoration already exists", + decoration: GraphqlRateLimitingCostDecoration{ + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("first"), + TypePath: kong.String("Query.users"), + AddConstant: kong.Float64(1.0), + }, + }, + wantErr: true, + }, + { + name: "returns an error if an id is not provided", + decoration: GraphqlRateLimitingCostDecoration{ + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + TypePath: kong.String("Query.users"), + AddConstant: kong.Float64(1.0), + }, + }, + wantErr: true, + }, + { + name: "returns an error if an empty decoration is provided", + decoration: GraphqlRateLimitingCostDecoration{ + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{}, + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if err := collection.Add(tt.decoration); (err != nil) != tt.wantErr { + t.Errorf("GraphqlRateLimitingCostDecorationsCollection.Add() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} + +func TestGraphqlRateLimitingCostDecorationGet(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + collection := graphqlRateLimitingCostDecorationsCollection() + + decoration := GraphqlRateLimitingCostDecoration{ + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("example"), + TypePath: kong.String("Query.users"), + AddConstant: kong.Float64(1.0), + }, + } + + err := collection.Add(decoration) + require.NoError(err, "error adding decoration") + + // Fetch the currently added entity + res, err := collection.Get("example") + require.NoError(err, "error getting decoration") + require.NotNil(res) + assert.Equal("example", *res.ID) + assert.Equal("Query.users", *res.TypePath) + assert.InDelta(1.0, *res.AddConstant, 0.001) + + // Fetch non-existent entity + res, err = collection.Get("does-not-exist") + require.Error(err) + require.Nil(res) +} + +func TestGraphqlRateLimitingCostDecorationGetByTypePath(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + collection := graphqlRateLimitingCostDecorationsCollection() + + decoration := GraphqlRateLimitingCostDecoration{ + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("typepath-example"), + TypePath: kong.String("Query.comments"), + AddConstant: kong.Float64(3.0), + }, + } + + err := collection.Add(decoration) + require.NoError(err, "error adding decoration") + + // Fetch by TypePath + res, err := collection.GetByTypePath("Query.comments") + require.NoError(err, "error getting decoration by TypePath") + require.NotNil(res) + assert.Equal("typepath-example", *res.ID) + assert.Equal("Query.comments", *res.TypePath) + + // Fetch non-existent TypePath + res, err = collection.GetByTypePath("Query.nonexistent") + require.Error(err) + require.Nil(res) + + // Fetch with empty TypePath + res, err = collection.GetByTypePath("") + require.Error(err) + require.Nil(res) +} + +func TestGraphqlRateLimitingCostDecorationUpdate(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + collection := graphqlRateLimitingCostDecorationsCollection() + + decoration := GraphqlRateLimitingCostDecoration{ + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("update-example"), + TypePath: kong.String("Query.items"), + AddConstant: kong.Float64(1.0), + }, + } + + err := collection.Add(decoration) + require.NoError(err, "error adding decoration") + + // Fetch the currently added entity + res, err := collection.Get("update-example") + require.NoError(err, "error getting decoration") + require.NotNil(res) + assert.InDelta(1.0, *res.AddConstant, 0.001) + + // Update AddConstant field + res.AddConstant = kong.Float64(5.0) + res.MulConstant = kong.Float64(2.0) + err = collection.Update(*res) + require.NoError(err, "error updating decoration") + + // Fetch again + res, err = collection.Get("update-example") + require.NoError(err, "error getting decoration") + require.NotNil(res) + assert.InDelta(5.0, *res.AddConstant, 0.001) + assert.InDelta(2.0, *res.MulConstant, 0.001) +} + +func TestGraphqlRateLimitingCostDecorationDelete(t *testing.T) { + require := require.New(t) + collection := graphqlRateLimitingCostDecorationsCollection() + + decoration := GraphqlRateLimitingCostDecoration{ + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("delete-example"), + TypePath: kong.String("Query.products"), + AddConstant: kong.Float64(1.0), + }, + } + + err := collection.Add(decoration) + require.NoError(err, "error adding decoration") + + // Fetch the currently added entity + res, err := collection.Get("delete-example") + require.NoError(err, "error getting decoration") + require.NotNil(res) + + // Delete entity + err = collection.Delete(*res.ID) + require.NoError(err, "error deleting decoration") + + // Fetch again + res, err = collection.Get("delete-example") + require.Error(err) + require.Nil(res) +} + +func TestGraphqlRateLimitingCostDecorationGetAll(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + collection := graphqlRateLimitingCostDecorationsCollection() + + populateGraphqlRateLimitingCostDecorations(t, collection) + + decorations, err := collection.GetAll() + require.NoError(err, "error getting all decorations") + assert.Len(decorations, 5) + assert.IsType([]*GraphqlRateLimitingCostDecoration{}, decorations) +} + +func populateGraphqlRateLimitingCostDecorations(t *testing.T, + collection *GraphqlRateLimitingCostDecorationsCollection, +) { + require := require.New(t) + decorations := []GraphqlRateLimitingCostDecoration{ + { + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("populate-first"), + TypePath: kong.String("Query.allUsers"), + AddConstant: kong.Float64(1.0), + }, + }, + { + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("populate-second"), + TypePath: kong.String("Query.allPosts"), + AddConstant: kong.Float64(2.0), + MulConstant: kong.Float64(1.5), + }, + }, + { + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("populate-third"), + TypePath: kong.String("Query.allComments"), + AddConstant: kong.Float64(1.0), + AddArguments: kong.StringSlice("limit"), + }, + }, + { + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("populate-fourth"), + TypePath: kong.String("Query.allProducts"), + MulConstant: kong.Float64(2.0), + MulArguments: kong.StringSlice("first", "last"), + }, + }, + { + GraphqlRateLimitingCostDecoration: kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("populate-fifth"), + TypePath: kong.String("Query.allOrders"), + AddConstant: kong.Float64(3.0), + MulConstant: kong.Float64(2.5), + AddArguments: kong.StringSlice("offset"), + MulArguments: kong.StringSlice("count"), + }, + }, + } + + for _, d := range decorations { + err := collection.Add(d) + require.NoError(err, "error adding decoration") + } +} diff --git a/pkg/state/state.go b/pkg/state/state.go index 52c150b4..18ae3418 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -33,16 +33,17 @@ type KongState struct { Keys *KeysCollection KeySets *KeySetsCollection - KeyAuths *KeyAuthsCollection - HMACAuths *HMACAuthsCollection - JWTAuths *JWTAuthsCollection - BasicAuths *BasicAuthsCollection - ACLGroups *ACLGroupsCollection - Oauth2Creds *Oauth2CredsCollection - MTLSAuths *MTLSAuthsCollection - DegraphqlRoutes *DegraphqlRoutesCollection - RBACRoles *RBACRolesCollection - RBACEndpointPermissions *RBACEndpointPermissionsCollection + KeyAuths *KeyAuthsCollection + HMACAuths *HMACAuthsCollection + JWTAuths *JWTAuthsCollection + BasicAuths *BasicAuthsCollection + ACLGroups *ACLGroupsCollection + Oauth2Creds *Oauth2CredsCollection + MTLSAuths *MTLSAuthsCollection + DegraphqlRoutes *DegraphqlRoutesCollection + GraphqlRateLimitingCostDecorations *GraphqlRateLimitingCostDecorationsCollection + RBACRoles *RBACRolesCollection + RBACEndpointPermissions *RBACEndpointPermissionsCollection // konnect-specific entities ServicePackages *ServicePackagesCollection @@ -60,6 +61,7 @@ func NewKongState() (*KongState, error) { oauth2CredsTemp := newOauth2CredsCollection(collection{}) mtlsAuthTemp := newMTLSAuthsCollection(collection{}) degraphqlRouteTemp := newDegraphqlRoutesCollection(collection{}) + graphqlRateLimitingCostDecorationTemp := newGraphqlRateLimitingCostDecorationsCollection(collection{}) schema := &memdb.DBSchema{ Tables: map[string]*memdb.TableSchema{ @@ -84,7 +86,8 @@ func NewKongState() (*KongState, error) { keyTableName: keyTableSchema, keySetTableName: keySetTableSchema, - degraphqlRouteTemp.TableName(): degraphqlRouteTemp.Schema(), + degraphqlRouteTemp.TableName(): degraphqlRouteTemp.Schema(), + graphqlRateLimitingCostDecorationTemp.TableName(): graphqlRateLimitingCostDecorationTemp.Schema(), keyAuthTemp.TableName(): keyAuthTemp.Schema(), hmacAuthTemp.TableName(): hmacAuthTemp.Schema(), @@ -133,6 +136,7 @@ func NewKongState() (*KongState, error) { state.KeySets = (*KeySetsCollection)(&state.common) state.DegraphqlRoutes = newDegraphqlRoutesCollection(state.common) + state.GraphqlRateLimitingCostDecorations = newGraphqlRateLimitingCostDecorationsCollection(state.common) state.KeyAuths = newKeyAuthsCollection(state.common) state.HMACAuths = newHMACAuthsCollection(state.common) diff --git a/pkg/state/types.go b/pkg/state/types.go index 48b01a85..4c261d90 100644 --- a/pkg/state/types.go +++ b/pkg/state/types.go @@ -1972,6 +1972,55 @@ func (d *DegraphqlRoute) EqualWithOpts(d2 *DegraphqlRoute, ignoreID bool) bool { return reflect.DeepEqual(d1Copy, d2Copy) } +// GraphqlRateLimitingCostDecoration represents a graphql rate limiting cost decoration in Kong. +type GraphqlRateLimitingCostDecoration struct { + kong.GraphqlRateLimitingCostDecoration `yaml:",inline"` + Meta +} + +// GetCustomEntityID returns the ID of the GraphqlRateLimitingCostDecoration. +func (g *GraphqlRateLimitingCostDecoration) GetCustomEntityID() string { + if g.ID == nil { + return "" + } + return *g.ID +} + +// GetCustomEntityType returns the GraphqlRateLimitingCostDecoration Type. +func (g *GraphqlRateLimitingCostDecoration) GetCustomEntityType() string { + return "graphql_ratelimiting_cost_decorations" +} + +// Console returns the string to identify the GraphqlRateLimitingCostDecoration. +func (g *GraphqlRateLimitingCostDecoration) Console() string { + if g.TypePath != nil { + return *g.TypePath + } + if g.ID != nil { + return *g.ID + } + return "" +} + +// Equal returns true if GraphqlRateLimitingCostDecoration g and g2 are equal. +func (g *GraphqlRateLimitingCostDecoration) Equal(g2 *GraphqlRateLimitingCostDecoration) bool { + return g.EqualWithOpts(g2, false) +} + +// EqualWithOpts returns true if GraphqlRateLimitingCostDecoration g and g2 are equal. +// If ignoreID is set to true, IDs will be ignored while comparison. +func (g *GraphqlRateLimitingCostDecoration) EqualWithOpts(g2 *GraphqlRateLimitingCostDecoration, ignoreID bool) bool { + g1Copy := g.DeepCopy() + g2Copy := g2.DeepCopy() + + if ignoreID { + g1Copy.ID = nil + g2Copy.ID = nil + } + + return reflect.DeepEqual(g1Copy, g2Copy) +} + // Partial represents a partial in Kong. // It adds some helper methods along with Meta to the original Partial object. type Partial struct { diff --git a/pkg/types/core.go b/pkg/types/core.go index 51b23f7f..940cd996 100644 --- a/pkg/types/core.go +++ b/pkg/types/core.go @@ -135,6 +135,9 @@ const ( DegraphqlRoute EntityType = "degraphql_routes" + // GraphqlRateLimitingCostDecoration identifies a GraphqlRateLimitingCostDecoration in Kong. + GraphqlRateLimitingCostDecoration EntityType = "graphql_ratelimiting_cost_decorations" + // Partial identifies a Partial in Kong. Partial EntityType = "partial" @@ -169,6 +172,8 @@ var AllTypes = []EntityType{ DegraphqlRoute, + GraphqlRateLimitingCostDecoration, + Partial, Key, KeySet, @@ -612,6 +617,21 @@ func NewEntity(t EntityType, opts EntityOpts) (Entity, error) { targetState: opts.TargetState, }, }, nil + case GraphqlRateLimitingCostDecoration: + return entityImpl{ + typ: GraphqlRateLimitingCostDecoration, + crudActions: &graphqlRateLimitingCostDecorationCRUD{ + client: opts.KongClient, + }, + postProcessActions: &graphqlRateLimitingCostDecorationPostAction{ + currentState: opts.CurrentState, + }, + differ: &graphqlRateLimitingCostDecorationDiffer{ + kind: entityTypeToKind(GraphqlRateLimitingCostDecoration), + currentState: opts.CurrentState, + targetState: opts.TargetState, + }, + }, nil case Partial: return entityImpl{ typ: Partial, diff --git a/pkg/types/graphql_ratelimiting_cost_decoration.go b/pkg/types/graphql_ratelimiting_cost_decoration.go new file mode 100644 index 00000000..5ab10247 --- /dev/null +++ b/pkg/types/graphql_ratelimiting_cost_decoration.go @@ -0,0 +1,198 @@ +package types + +import ( + "context" + "errors" + "fmt" + + "github.com/kong/go-database-reconciler/pkg/crud" + "github.com/kong/go-database-reconciler/pkg/state" + "github.com/kong/go-kong/kong" +) + +// graphqlRateLimitingCostDecorationCRUD implements crud.Actions interface. +type graphqlRateLimitingCostDecorationCRUD struct { + client *kong.Client +} + +func graphqlRateLimitingCostDecorationFromStruct(arg crud.Event) *state.GraphqlRateLimitingCostDecoration { + decoration, ok := arg.Obj.(*state.GraphqlRateLimitingCostDecoration) + if !ok { + panic("unexpected type, expected *state.GraphqlRateLimitingCostDecoration") + } + return decoration +} + +// Create creates a GraphqlRateLimitingCostDecoration in Kong. +// The arg should be of type crud.Event, containing the decoration to be created, +// else the function will panic. +// It returns the created *state.GraphqlRateLimitingCostDecoration. +func (s *graphqlRateLimitingCostDecorationCRUD) Create(ctx context.Context, arg ...crud.Arg) (crud.Arg, error) { + event := crud.EventFromArg(arg[0]) + decoration := graphqlRateLimitingCostDecorationFromStruct(event) + + createdDecoration, err := s.client.GraphqlRateLimitingCostDecorations.CreateWithID(ctx, + &decoration.GraphqlRateLimitingCostDecoration) + if err != nil { + return nil, err + } + return &state.GraphqlRateLimitingCostDecoration{GraphqlRateLimitingCostDecoration: *createdDecoration}, nil +} + +// Delete deletes a GraphqlRateLimitingCostDecoration in Kong. +// The arg should be of type crud.Event, containing the decoration to be deleted, +// else the function will panic. +// It returns the deleted *state.GraphqlRateLimitingCostDecoration. +func (s *graphqlRateLimitingCostDecorationCRUD) Delete(ctx context.Context, arg ...crud.Arg) (crud.Arg, error) { + event := crud.EventFromArg(arg[0]) + decoration := graphqlRateLimitingCostDecorationFromStruct(event) + err := s.client.GraphqlRateLimitingCostDecorations.Delete(ctx, decoration.ID) + if err != nil { + return nil, err + } + return decoration, nil +} + +// Update updates a GraphqlRateLimitingCostDecoration in Kong. +// The arg should be of type crud.Event, containing the decoration to be updated, +// else the function will panic. +// It returns the updated *state.GraphqlRateLimitingCostDecoration. +func (s *graphqlRateLimitingCostDecorationCRUD) Update(ctx context.Context, arg ...crud.Arg) (crud.Arg, error) { + event := crud.EventFromArg(arg[0]) + decoration := graphqlRateLimitingCostDecorationFromStruct(event) + + updatedDecoration, err := s.client.GraphqlRateLimitingCostDecorations.Update(ctx, + &decoration.GraphqlRateLimitingCostDecoration) + if err != nil { + return nil, err + } + return &state.GraphqlRateLimitingCostDecoration{GraphqlRateLimitingCostDecoration: *updatedDecoration}, nil +} + +type graphqlRateLimitingCostDecorationDiffer struct { + kind crud.Kind + + currentState, targetState *state.KongState +} + +func (d *graphqlRateLimitingCostDecorationDiffer) Deletes(handler func(crud.Event) error) error { + currentDecorations, err := d.currentState.GraphqlRateLimitingCostDecorations.GetAll() + if err != nil { + return fmt.Errorf("error fetching graphql ratelimiting cost decorations from state: %w", err) + } + + for _, decoration := range currentDecorations { + n, err := d.deleteDecoration(decoration) + if err != nil { + return err + } + if n != nil { + err = handler(*n) + if err != nil { + return err + } + } + } + return nil +} + +func (d *graphqlRateLimitingCostDecorationDiffer) deleteDecoration( + decoration *state.GraphqlRateLimitingCostDecoration, +) (*crud.Event, error) { + // First try to find by ID + _, err := d.targetState.GraphqlRateLimitingCostDecorations.Get(*decoration.ID) + if err == nil { + // Found by ID, no delete needed + return nil, nil + } + if !errors.Is(err, state.ErrNotFound) { + return nil, fmt.Errorf("looking up graphql ratelimiting cost decoration %q: %w", *decoration.ID, err) + } + + // Not found by ID, try to find by TypePath + if decoration.TypePath != nil { + _, err = d.targetState.GraphqlRateLimitingCostDecorations.GetByTypePath(*decoration.TypePath) + if err == nil { + // Found by TypePath, no delete needed + return nil, nil + } + if !errors.Is(err, state.ErrNotFound) { + return nil, fmt.Errorf("looking up graphql ratelimiting cost decoration by type_path %q: %w", + *decoration.TypePath, err) + } + } + + // Not found by ID or TypePath, delete it + return &crud.Event{ + Op: crud.Delete, + Kind: d.kind, + Obj: decoration, + }, nil +} + +func (d *graphqlRateLimitingCostDecorationDiffer) CreateAndUpdates(handler func(crud.Event) error) error { + targetDecorations, err := d.targetState.GraphqlRateLimitingCostDecorations.GetAll() + if err != nil { + return fmt.Errorf("error fetching graphql ratelimiting cost decorations from state: %w", err) + } + + for _, decoration := range targetDecorations { + n, err := d.createUpdateDecoration(decoration) + if err != nil { + return err + } + if n != nil { + err = handler(*n) + if err != nil { + return err + } + } + } + return nil +} + +func (d *graphqlRateLimitingCostDecorationDiffer) createUpdateDecoration( + decoration *state.GraphqlRateLimitingCostDecoration, +) (*crud.Event, error) { + decoration = &state.GraphqlRateLimitingCostDecoration{GraphqlRateLimitingCostDecoration: *decoration.DeepCopy()} + + // First try to find by ID + currentDecoration, err := d.currentState.GraphqlRateLimitingCostDecorations.Get(*decoration.ID) + if err != nil && !errors.Is(err, state.ErrNotFound) { + return nil, fmt.Errorf("error looking up graphql ratelimiting cost decoration %q: %w", + *decoration.ID, err) + } + + // If not found by ID, try to find by TypePath + if errors.Is(err, state.ErrNotFound) && decoration.TypePath != nil { + currentDecoration, err = d.currentState.GraphqlRateLimitingCostDecorations.GetByTypePath(*decoration.TypePath) + if err != nil && !errors.Is(err, state.ErrNotFound) { + return nil, fmt.Errorf("error looking up graphql ratelimiting cost decoration by type_path %q: %w", + *decoration.TypePath, err) + } + // If found by TypePath, use the existing ID + if err == nil && currentDecoration != nil { + decoration.ID = currentDecoration.ID + } + } + + if errors.Is(err, state.ErrNotFound) { + // decoration not present, create it + return &crud.Event{ + Op: crud.Create, + Kind: d.kind, + Obj: decoration, + }, nil + } + + // found, check if update needed + if !currentDecoration.EqualWithOpts(decoration, false) { + return &crud.Event{ + Op: crud.Update, + Kind: d.kind, + Obj: decoration, + OldObj: currentDecoration, + }, nil + } + return nil, nil +} diff --git a/pkg/types/postProcess.go b/pkg/types/postProcess.go index 63757fff..be963968 100644 --- a/pkg/types/postProcess.go +++ b/pkg/types/postProcess.go @@ -529,6 +529,25 @@ func (crud *degraphqlRoutePostAction) Update(_ context.Context, args ...crud.Arg return nil, crud.currentState.DegraphqlRoutes.Update(*args[0].(*state.DegraphqlRoute)) } +type graphqlRateLimitingCostDecorationPostAction struct { + currentState *state.KongState +} + +func (crud *graphqlRateLimitingCostDecorationPostAction) Create(_ context.Context, args ...crud.Arg) (crud.Arg, error) { + return nil, + crud.currentState.GraphqlRateLimitingCostDecorations.Add(*args[0].(*state.GraphqlRateLimitingCostDecoration)) +} + +func (crud *graphqlRateLimitingCostDecorationPostAction) Delete(_ context.Context, args ...crud.Arg) (crud.Arg, error) { + return nil, crud.currentState.GraphqlRateLimitingCostDecorations.Delete( + *((args[0].(*state.GraphqlRateLimitingCostDecoration)).ID)) +} + +func (crud *graphqlRateLimitingCostDecorationPostAction) Update(_ context.Context, args ...crud.Arg) (crud.Arg, error) { + return nil, + crud.currentState.GraphqlRateLimitingCostDecorations.Update(*args[0].(*state.GraphqlRateLimitingCostDecoration)) +} + type partialPostAction struct { currentState *state.KongState } diff --git a/pkg/utils/tags_test.go b/pkg/utils/tags_test.go index 304c1b90..09b4812d 100644 --- a/pkg/utils/tags_test.go +++ b/pkg/utils/tags_test.go @@ -49,7 +49,13 @@ func equalArray(want, have []*string) bool { if len(want) != len(have) { return false } - for i := 0; i < len(want); i++ { + for i := range want { + if want[i] == nil || have[i] == nil { + if want[i] != have[i] { + return false + } + continue + } if *want[i] != *have[i] { return false } diff --git a/pkg/utils/types.go b/pkg/utils/types.go index d1c4c28f..226c0670 100644 --- a/pkg/utils/types.go +++ b/pkg/utils/types.go @@ -55,7 +55,8 @@ type KongRawState struct { Oauth2Creds []*kong.Oauth2Credential MTLSAuths []*kong.MTLSAuth - DegraphqlRoutes []*kong.DegraphqlRoute + DegraphqlRoutes []*kong.DegraphqlRoute + GraphqlRateLimitingCostDecorations []*kong.GraphqlRateLimitingCostDecoration RBACRoles []*kong.RBACRole RBACEndpointPermissions []*kong.RBACEndpointPermission diff --git a/tests/integration/apply_test.go b/tests/integration/apply_test.go index 92a077b1..5285d3b3 100644 --- a/tests/integration/apply_test.go +++ b/tests/integration/apply_test.go @@ -30,12 +30,30 @@ func Test_Apply_Custom_Entities(t *testing.T) { initialStateFile: "testdata/apply/001-custom-entities/initial-state.yaml", targetPartialStateFile: "testdata/apply/001-custom-entities/partial-update.yaml", }, + { + name: "custom entity - graphql_ratelimiting_cost_decorations basic", + initialStateFile: "testdata/apply/001-custom-entities/initial-state.yaml", + targetPartialStateFile: "testdata/apply/001-custom-entities/graphql-cost-decoration-basic.yaml", + }, + { + name: "custom entity - graphql_ratelimiting_cost_decorations multiple", + initialStateFile: "testdata/apply/001-custom-entities/initial-state.yaml", + targetPartialStateFile: "testdata/apply/001-custom-entities/graphql-cost-decoration-multiple.yaml", + }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { + // Clean up any existing graphql_ratelimiting_cost_decorations before each test + existingDecorations, err := client.GraphqlRateLimitingCostDecorations.ListAll(ctx) + if err == nil { + for _, d := range existingDecorations { + _ = client.GraphqlRateLimitingCostDecorations.Delete(ctx, d.ID) + } + } + mustResetKongState(ctx, t, client, deckDump.Config{}) - err := sync(tc.initialStateFile) + err = sync(tc.initialStateFile) require.NoError(t, err) err = apply(tc.targetPartialStateFile) diff --git a/tests/integration/dump_test.go b/tests/integration/dump_test.go index a585aa3e..308a4f9f 100644 --- a/tests/integration/dump_test.go +++ b/tests/integration/dump_test.go @@ -416,6 +416,205 @@ func Test_Dump_CustomEntities(t *testing.T) { require.Equal(t, "query{ name }", query) } +func Test_Dump_GraphqlRateLimitingCostDecorations(t *testing.T) { + kong.RunWhenEnterprise(t, ">=3.0.0", kong.RequiredFeatures{}) + setup(t) + + client, err := getTestClient() + require.NoError(t, err) + + // Clean up any existing decorations before starting the test + existingDecorations, err := client.GraphqlRateLimitingCostDecorations.ListAll(context.Background()) + require.NoError(t, err) + for _, d := range existingDecorations { + _ = client.GraphqlRateLimitingCostDecorations.Delete(context.Background(), d.ID) + } + + // Create a graphql_ratelimiting_cost_decoration using the dedicated client + decoration, err := client.GraphqlRateLimitingCostDecorations.CreateWithID(context.Background(), &kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("d5308258-3c34-4f28-94f9-52e3a8a6c4b1"), + TypePath: kong.String("Query.users"), + AddConstant: kong.Float64(1.5), + MulConstant: kong.Float64(2.0), + AddArguments: kong.StringSlice("limit"), + MulArguments: kong.StringSlice("first", "last"), + }) + require.NoError(t, err, "Should create graphql_ratelimiting_cost_decoration successfully") + t.Logf("Created graphql_ratelimiting_cost_decoration %s with type_path %s", *decoration.ID, *decoration.TypePath) + + // Clean up after the test + t.Cleanup(func() { + err := client.GraphqlRateLimitingCostDecorations.Delete(context.Background(), decoration.ID) + require.NoError(t, err, "should delete graphql_ratelimiting_cost_decoration in cleanup") + }) + + // Call dump.Get with custom entities + rawState, err := deckDump.Get(context.Background(), client, deckDump.Config{ + CustomEntityTypes: []string{"graphql_ratelimiting_cost_decorations"}, + }) + require.NoError(t, err, "Should dump from Kong successfully") + require.Len(t, rawState.CustomEntities, 1, "Dumped raw state should contain 1 custom entity") + + // Check entity type + typ := rawState.CustomEntities[0].Type() + require.Equal(t, custom.Type("graphql_ratelimiting_cost_decorations"), typ, + "Entity should have type graphql_ratelimiting_cost_decorations") + + // Check fields of the entity + obj := rawState.CustomEntities[0].Object() + + typePath, ok := obj["type_path"].(string) + require.Truef(t, ok, "'type_path' field should have type 'string' but actual '%T'", obj["type_path"]) + require.Equal(t, "Query.users", typePath) + + addConstant, ok := obj["add_constant"].(float64) + require.Truef(t, ok, "'add_constant' field should have type 'float64' but actual '%T'", obj["add_constant"]) + require.Equal(t, 1.5, addConstant) + + mulConstant, ok := obj["mul_constant"].(float64) + require.Truef(t, ok, "'mul_constant' field should have type 'float64' but actual '%T'", obj["mul_constant"]) + require.Equal(t, 2.0, mulConstant) +} + +func Test_Dump_GraphqlRateLimitingCostDecorations_Multiple(t *testing.T) { + kong.RunWhenEnterprise(t, ">=3.4.0", kong.RequiredFeatures{}) + setup(t) + + client, err := getTestClient() + require.NoError(t, err) + + // Clean up any existing decorations before starting the test + existingDecorations, err := client.GraphqlRateLimitingCostDecorations.ListAll(context.Background()) + require.NoError(t, err) + for _, d := range existingDecorations { + _ = client.GraphqlRateLimitingCostDecorations.Delete(context.Background(), d.ID) + } + + // Create multiple graphql_ratelimiting_cost_decorations + decoration1, err := client.GraphqlRateLimitingCostDecorations.CreateWithID(context.Background(), &kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("a1b2c3d4-1111-2222-3333-444455556666"), + TypePath: kong.String("Query.users"), + AddConstant: kong.Float64(1.0), + }) + require.NoError(t, err, "Should create first decoration successfully") + + decoration2, err := client.GraphqlRateLimitingCostDecorations.CreateWithID(context.Background(), &kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("a1b2c3d4-2222-3333-4444-555566667777"), + TypePath: kong.String("Query.posts"), + AddConstant: kong.Float64(2.0), + }) + require.NoError(t, err, "Should create second decoration successfully") + + decoration3, err := client.GraphqlRateLimitingCostDecorations.CreateWithID(context.Background(), &kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("a1b2c3d4-3333-4444-5555-666677778888"), + TypePath: kong.String("Mutation.createUser"), + MulConstant: kong.Float64(3.0), + MulArguments: kong.StringSlice("count"), + }) + require.NoError(t, err, "Should create third decoration successfully") + + // Clean up after the test + t.Cleanup(func() { + _ = client.GraphqlRateLimitingCostDecorations.Delete(context.Background(), decoration1.ID) + _ = client.GraphqlRateLimitingCostDecorations.Delete(context.Background(), decoration2.ID) + _ = client.GraphqlRateLimitingCostDecorations.Delete(context.Background(), decoration3.ID) + }) + + // Call dump.Get with custom entities + rawState, err := deckDump.Get(context.Background(), client, deckDump.Config{ + CustomEntityTypes: []string{"graphql_ratelimiting_cost_decorations"}, + }) + require.NoError(t, err, "Should dump from Kong successfully") + require.Len(t, rawState.CustomEntities, 3, "Dumped raw state should contain 3 custom entities") + + // Verify all entities have the correct type + for _, entity := range rawState.CustomEntities { + require.Equal(t, custom.Type("graphql_ratelimiting_cost_decorations"), entity.Type()) + } + + // Collect all type_paths from dumped entities + typePaths := make(map[string]bool) + for _, entity := range rawState.CustomEntities { + obj := entity.Object() + typePath, ok := obj["type_path"].(string) + require.True(t, ok, "type_path should be a string") + typePaths[typePath] = true + } + + // Verify all expected type_paths are present + require.True(t, typePaths["Query.users"], "Should contain Query.users") + require.True(t, typePaths["Query.posts"], "Should contain Query.posts") + require.True(t, typePaths["Mutation.createUser"], "Should contain Mutation.createUser") +} + +func Test_Dump_GraphqlRateLimitingCostDecorations_EmptyWhenNoneExist(t *testing.T) { + kong.RunWhenEnterprise(t, ">=3.4.0", kong.RequiredFeatures{}) + setup(t) + + client, err := getTestClient() + require.NoError(t, err) + + // Clean up ALL existing decorations + existingDecorations, err := client.GraphqlRateLimitingCostDecorations.ListAll(context.Background()) + require.NoError(t, err) + for _, d := range existingDecorations { + _ = client.GraphqlRateLimitingCostDecorations.Delete(context.Background(), d.ID) + } + + // Dump when no decorations exist + rawState, err := deckDump.Get(context.Background(), client, deckDump.Config{ + CustomEntityTypes: []string{"graphql_ratelimiting_cost_decorations"}, + }) + require.NoError(t, err, "Should dump successfully even when no decorations exist") + require.Len(t, rawState.CustomEntities, 0, "Should have no custom entities when none exist") +} + +func Test_Dump_GraphqlRateLimitingCostDecorations_MixedWithOtherCustomEntities(t *testing.T) { + kong.RunWhenEnterprise(t, ">=3.4.0", kong.RequiredFeatures{}) + setup(t) + + client, err := getTestClient() + require.NoError(t, err) + + // Clean up any existing decorations + existingDecorations, err := client.GraphqlRateLimitingCostDecorations.ListAll(context.Background()) + require.NoError(t, err) + for _, d := range existingDecorations { + _ = client.GraphqlRateLimitingCostDecorations.Delete(context.Background(), d.ID) + } + + // Create a decoration + decoration, err := client.GraphqlRateLimitingCostDecorations.CreateWithID(context.Background(), &kong.GraphqlRateLimitingCostDecoration{ + ID: kong.String("b2c3d4e5-4444-5555-6666-777788889999"), + TypePath: kong.String("Query.mixed"), + AddConstant: kong.Float64(1.0), + }) + require.NoError(t, err) + + t.Cleanup(func() { + _ = client.GraphqlRateLimitingCostDecorations.Delete(context.Background(), decoration.ID) + }) + + // Dump with multiple custom entity types (both valid) + rawState, err := deckDump.Get(context.Background(), client, deckDump.Config{ + CustomEntityTypes: []string{"graphql_ratelimiting_cost_decorations", "degraphql_routes"}, + }) + require.NoError(t, err, "Should dump successfully with multiple custom entity types") + + // Should have at least our decoration + found := false + for _, entity := range rawState.CustomEntities { + if entity.Type() == "graphql_ratelimiting_cost_decorations" { + obj := entity.Object() + if obj["type_path"] == "Query.mixed" { + found = true + break + } + } + } + require.True(t, found, "Should find our graphql_ratelimiting_cost_decoration in mixed dump") +} + func Test_Dump_KeysAndKeySets(t *testing.T) { runWhen(t, "kong", ">=3.1.0") setup(t) diff --git a/tests/integration/sync_test.go b/tests/integration/sync_test.go index 54476b1d..307e647e 100644 --- a/tests/integration/sync_test.go +++ b/tests/integration/sync_test.go @@ -9399,6 +9399,89 @@ func Test_Sync_DegraphqlRoutes(t *testing.T) { }) } +func Test_Sync_GraphqlRateLimitingCostDecorations(t *testing.T) { + client, err := getTestClient() + if err != nil { + t.Fatal(err.Error()) + } + + ctx := context.Background() + dumpConfig := deckDump.Config{CustomEntityTypes: []string{"graphql_ratelimiting_cost_decorations"}} + + runWhen(t, "enterprise", ">=3.0.0") + + t.Run("create graphql ratelimiting cost decoration", func(t *testing.T) { + mustResetKongState(ctx, t, client, dumpConfig) + currentState, err := fetchCurrentState(ctx, client, dumpConfig) + require.NoError(t, err) + + targetState := stateFromFile(ctx, t, "testdata/sync/047-graphql-ratelimiting-cost-decorations/kong.yaml", client, dumpConfig) + syncer, err := deckDiff.NewSyncer(deckDiff.SyncerOpts{ + CurrentState: currentState, + TargetState: targetState, + + KongClient: client, + }) + require.NoError(t, err) + + stats, errs, changes := syncer.Solve(ctx, 1, false, true) + require.Len(t, errs, 0, "Should have no errors in syncing") + logEntityChanges(t, stats, changes) + + newState, err := fetchCurrentState(ctx, client, dumpConfig) + require.NoError(t, err) + + decorations, err := newState.GraphqlRateLimitingCostDecorations.GetAll() + require.NoError(t, err) + + assert.Equal(t, 1, len(decorations)) + assert.Equal(t, "Query.users", *decorations[0].TypePath) + assert.Equal(t, 1.0, *decorations[0].AddConstant) + }) + + t.Run("create graphql ratelimiting cost decoration - all fields", func(t *testing.T) { + mustResetKongState(ctx, t, client, dumpConfig) + currentState, err := fetchCurrentState(ctx, client, dumpConfig) + require.NoError(t, err) + + targetState := stateFromFile(ctx, t, "testdata/sync/047-graphql-ratelimiting-cost-decorations/kong-all-fields.yaml", client, dumpConfig) + syncer, err := deckDiff.NewSyncer(deckDiff.SyncerOpts{ + CurrentState: currentState, + TargetState: targetState, + + KongClient: client, + }) + require.NoError(t, err) + + stats, errs, changes := syncer.Solve(ctx, 1, false, true) + require.Len(t, errs, 0, "Should have no errors in syncing") + logEntityChanges(t, stats, changes) + + newState, err := fetchCurrentState(ctx, client, dumpConfig) + require.NoError(t, err) + + decorations, err := newState.GraphqlRateLimitingCostDecorations.GetAll() + require.NoError(t, err) + + assert.Equal(t, 1, len(decorations)) + assert.Equal(t, "Query.posts", *decorations[0].TypePath) + assert.Equal(t, 2.0, *decorations[0].AddConstant) + assert.Equal(t, 1.5, *decorations[0].MulConstant) + + expectedAddArgs := kong.StringSlice("limit", "offset") + assert.Equal(t, expectedAddArgs, decorations[0].AddArguments) + + expectedMulArgs := kong.StringSlice("first", "last") + assert.Equal(t, expectedMulArgs, decorations[0].MulArguments) + }) + + t.Run("create graphql ratelimiting cost decoration - fails if type_path is missing", func(t *testing.T) { + err := sync("testdata/sync/047-graphql-ratelimiting-cost-decorations/kong-missing-type-path.yaml") + require.Error(t, err) + assert.ErrorContains(t, err, "type_path is required") + }) +} + func Test_Sync_CustomEntities_Fake(t *testing.T) { runWhen(t, "enterprise", ">=3.0.0") setup(t) diff --git a/tests/integration/testdata/apply/001-custom-entities/graphql-cost-decoration-basic.yaml b/tests/integration/testdata/apply/001-custom-entities/graphql-cost-decoration-basic.yaml new file mode 100644 index 00000000..70d930a3 --- /dev/null +++ b/tests/integration/testdata/apply/001-custom-entities/graphql-cost-decoration-basic.yaml @@ -0,0 +1,6 @@ +_format_version: "3.0" +custom_entities: +- type: graphql_ratelimiting_cost_decorations + fields: + type_path: "Query.users" + add_constant: 1.0 diff --git a/tests/integration/testdata/apply/001-custom-entities/graphql-cost-decoration-multiple.yaml b/tests/integration/testdata/apply/001-custom-entities/graphql-cost-decoration-multiple.yaml new file mode 100644 index 00000000..108f346b --- /dev/null +++ b/tests/integration/testdata/apply/001-custom-entities/graphql-cost-decoration-multiple.yaml @@ -0,0 +1,14 @@ +_format_version: "3.0" +custom_entities: +- type: graphql_ratelimiting_cost_decorations + fields: + type_path: "Query.users" + add_constant: 2.0 + mul_constant: 1.5 +- type: graphql_ratelimiting_cost_decorations + fields: + type_path: "Query.posts" + add_constant: 3.0 + add_arguments: + - "limit" + - "offset" diff --git a/tests/integration/testdata/sync/047-graphql-ratelimiting-cost-decorations/kong-all-fields.yaml b/tests/integration/testdata/sync/047-graphql-ratelimiting-cost-decorations/kong-all-fields.yaml new file mode 100644 index 00000000..9f79983e --- /dev/null +++ b/tests/integration/testdata/sync/047-graphql-ratelimiting-cost-decorations/kong-all-fields.yaml @@ -0,0 +1,22 @@ +_format_version: "3.0" +services: +- connect_timeout: 60000 + host: mockbin.org + name: svc1 + port: 80 + protocol: http + read_timeout: 60000 + retries: 5 + write_timeout: 60000 +custom_entities: + - type: graphql_ratelimiting_cost_decorations + fields: + type_path: "Query.posts" + add_constant: 2.0 + mul_constant: 1.5 + add_arguments: + - "limit" + - "offset" + mul_arguments: + - "first" + - "last" diff --git a/tests/integration/testdata/sync/047-graphql-ratelimiting-cost-decorations/kong-missing-type-path.yaml b/tests/integration/testdata/sync/047-graphql-ratelimiting-cost-decorations/kong-missing-type-path.yaml new file mode 100644 index 00000000..57ab98c2 --- /dev/null +++ b/tests/integration/testdata/sync/047-graphql-ratelimiting-cost-decorations/kong-missing-type-path.yaml @@ -0,0 +1,5 @@ +_format_version: "3.0" +custom_entities: + - type: graphql_ratelimiting_cost_decorations + fields: + add_constant: 1.0 diff --git a/tests/integration/testdata/sync/047-graphql-ratelimiting-cost-decorations/kong.yaml b/tests/integration/testdata/sync/047-graphql-ratelimiting-cost-decorations/kong.yaml new file mode 100644 index 00000000..a58d927e --- /dev/null +++ b/tests/integration/testdata/sync/047-graphql-ratelimiting-cost-decorations/kong.yaml @@ -0,0 +1,25 @@ +_format_version: "3.0" +plugins: + - config: + graphql_server_path: /graphql + enabled: true + name: degraphql + protocols: + - grpc + - grpcs + - http + - https +services: + - connect_timeout: 60000 + host: mockbin.org + name: svc1 + port: 80 + protocol: http + read_timeout: 60000 + retries: 5 + write_timeout: 60000 +custom_entities: + - type: graphql_ratelimiting_cost_decorations + fields: + type_path: "Query.users" + add_constant: 1.0