diff --git a/.gitattributes b/.gitattributes index 25e3302a42b..e032f59a361 100644 --- a/.gitattributes +++ b/.gitattributes @@ -9,3 +9,5 @@ docs/theme/js/theme.js linguist-vendored=false *_pb.ts linguist-generated *_pb.grpc-*.ts linguist-generated *_pb.client.ts linguist-generated + +*.golden -text diff --git a/go.mod b/go.mod index 3336b6707ce..c25b17dd018 100644 --- a/go.mod +++ b/go.mod @@ -206,6 +206,8 @@ require ( github.com/spiffe/aws-spiffe-workload-helper v0.0.4 github.com/spiffe/go-spiffe/v2 v2.5.0 github.com/stretchr/testify v1.10.0 + github.com/testcontainers/testcontainers-go v0.37.0 + github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0 github.com/tidwall/pretty v1.2.1 github.com/tidwall/sjson v1.2.5 github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb @@ -337,6 +339,7 @@ require ( github.com/containerd/log v0.1.0 // indirect github.com/containerd/platforms v0.2.1 // indirect github.com/containerd/stargz-snapshotter/estargz v0.16.3 // indirect + github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/crewjam/httperr v0.2.0 // indirect github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 // indirect github.com/cyphar/filepath-securejoin v0.4.1 // indirect @@ -454,6 +457,7 @@ require ( github.com/lithammer/dedent v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect + github.com/magiconair/properties v1.8.10 // indirect github.com/mailgun/errors v0.4.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect @@ -469,11 +473,17 @@ require ( github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect + github.com/moby/go-archive v0.1.0 // indirect + github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/spdystream v0.5.0 // indirect + github.com/moby/sys/sequential v0.6.0 // indirect + github.com/moby/sys/user v0.4.0 // indirect + github.com/moby/sys/userns v0.1.0 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/montanaflynn/stats v0.7.0 // indirect + github.com/morikuni/aec v1.0.0 // indirect github.com/mtibben/percent v0.2.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect diff --git a/go.sum b/go.sum index 574ad594849..473d462d1f1 100644 --- a/go.sum +++ b/go.sum @@ -653,8 +653,8 @@ github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMb github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4= github.com/99designs/keyring v1.2.2 h1:pZd3neh/EmUzWONb35LxQfvuY7kiSXAq3HQd97+XBn0= github.com/99designs/keyring v1.2.2/go.mod h1:wes/FrByc8j7lFOAGLGSNEg8f/PaI3cgTBqhFkHUrPk= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24 h1:bvDV9vkmnHYOMsOr4WLk+Vo07yKIzd94sVoIqshQ4bU= -github.com/AdaLogics/go-fuzz-headers v0.0.0-20230811130428-ced1acdcaa24/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d h1:zjqpY4C7H15HjRPEenkS4SAn3Jy2eRRjkjZbGR30TOg= github.com/AdamKorcz/go-fuzz-headers-1 v0.0.0-20230919221257-8b5d3ce2d11d/go.mod h1:XNqJ7hv2kY++g8XEHREpi+JqZo3+0l+CH2egBVN4yqM= github.com/AliyunContainerService/ack-ram-tool/pkg/credentials/provider v0.14.0 h1:kcnfY4vljxXliXDBrA9K9lwF8IoEZ4Up6Eg9kWTIm28= @@ -1111,6 +1111,8 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7 github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -1806,6 +1808,8 @@ github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2 github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailgun/errors v0.4.0 h1:6LFBvod6VIW83CMIOT9sYNp28TCX0NejFPP4dSX++i8= github.com/mailgun/errors v0.4.0/go.mod h1:xGBaaKdEdQT0/FhwvoXv4oBaqqmVZz9P1XEnvD/onc0= github.com/mailgun/mailgun-go/v4 v4.23.0 h1:jPEMJzzin2s7lvehcfv/0UkyBu18GvcURPr2+xtZRbk= @@ -1873,12 +1877,20 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -2191,6 +2203,10 @@ github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d h1:vfofYNRScrDd github.com/syndtr/goleveldb v1.0.1-0.20220721030215-126854af5e6d/go.mod h1:RRCYJbIwD5jmqPI9XoAFR0OcDxqUctll6zUj/+B4S48= github.com/tchap/go-patricia/v2 v2.3.2 h1:xTHFutuitO2zqKAQ5rCROYgUb7Or/+IC3fts9/Yc7nM= github.com/tchap/go-patricia/v2 v2.3.2/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= +github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg= +github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= +github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0 h1:LqUos1oR5iuuzorFnSvxsHNdYdCHB/DfI82CuT58wbI= +github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0/go.mod h1:vHEEHx5Kf+uq5hveaVAMrTzPY8eeRZcKcl23MRw5Tkc= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= @@ -3189,8 +3205,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw= -gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= -gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= +gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= +gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ= gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= helm.sh/helm/v3 v3.18.4 h1:pNhnHM3nAmDrxz6/UC+hfjDY4yeDATQCka2/87hkZXQ= diff --git a/integrations/terraform-mwi/go.sum b/integrations/terraform-mwi/go.sum index 921efaf514c..835523645b3 100644 --- a/integrations/terraform-mwi/go.sum +++ b/integrations/terraform-mwi/go.sum @@ -1006,6 +1006,8 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7 github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -1059,6 +1061,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 h1:0+BTyxIYgiVAry/P5s8R4dYuLkhB9Nhso8ogFWNr4IQ= github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8/go.mod h1:JkjcmqbLW+khwt2fmBPJFBhx2zGZ8XobRZ+O0VhlwWo= github.com/emicklei/go-restful/v3 v3.11.3 h1:yagOQz/38xJmcNeZJtrUcKjkHRltIaIFXKWeG1SkWGE= @@ -1166,6 +1170,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= @@ -1710,9 +1716,13 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis= @@ -1767,8 +1777,18 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1781,6 +1801,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= @@ -1878,6 +1900,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= @@ -1953,6 +1977,9 @@ github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df h1:S77Pf5fIG github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM= +github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -2035,6 +2062,10 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg= +github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= +github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0 h1:LqUos1oR5iuuzorFnSvxsHNdYdCHB/DfI82CuT58wbI= +github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0/go.mod h1:vHEEHx5Kf+uq5hveaVAMrTzPY8eeRZcKcl23MRw5Tkc= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= @@ -2062,6 +2093,10 @@ github.com/tink-crypto/tink-go/v2 v2.4.0 h1:8VPZeZI4EeZ8P/vB6SIkhlStrJfivTJn+cQ4 github.com/tink-crypto/tink-go/v2 v2.4.0/go.mod h1:l//evrF2Y3MjdbpNDNGnKgCpo5zSmvUvnQ4MU+yE2sw= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26 h1:YTbkeFbzcer+42bIgo6Za2194nKwhZPgaZKsP76QffE= github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26/go.mod h1:ODywn0gGarHMMdSkWT56ULoK8Hk71luOyRseKek9COw= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= @@ -2120,6 +2155,8 @@ github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUei github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zclconf/go-cty v1.16.3 h1:osr++gw2T61A8KVYHoQiFbFd1Lh3JOCXc/jFLJXKTxk= diff --git a/integrations/terraform/go.sum b/integrations/terraform/go.sum index 8cb75eeb86f..5e4680202ff 100644 --- a/integrations/terraform/go.sum +++ b/integrations/terraform/go.sum @@ -1029,6 +1029,8 @@ github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7 github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +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= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -1082,6 +1084,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dvsekhvalnov/jose2go v1.6.0 h1:Y9gnSnP4qEI0+/uQkHvFXeD2PLPJeXEL+ySMEA2EjTY= github.com/dvsekhvalnov/jose2go v1.6.0/go.mod h1:QsHjhyTlD/lAVqn/NSbVZmSCGeDehTB/mPZadG+mhXU= +github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw= +github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ= github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8 h1:0+BTyxIYgiVAry/P5s8R4dYuLkhB9Nhso8ogFWNr4IQ= github.com/elimity-com/scim v0.0.0-20240320110924-172bf2aee9c8/go.mod h1:JkjcmqbLW+khwt2fmBPJFBhx2zGZ8XobRZ+O0VhlwWo= github.com/emicklei/go-restful/v3 v3.11.3 h1:yagOQz/38xJmcNeZJtrUcKjkHRltIaIFXKWeG1SkWGE= @@ -1198,6 +1202,8 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= +github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= +github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= github.com/go-openapi/errors v0.22.1 h1:kslMRRnK7NCb/CvR1q1VWuEQCEIsBGn5GgKD9e+HYhU= @@ -1766,9 +1772,13 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= +github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= +github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE= +github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.9.0 h1:PrnmzHw7262yW8sTBwxi1PdJA3Iw/EKBa8psRf7d9a4= github.com/mailru/easyjson v0.9.0/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= github.com/mark3labs/mcp-go v0.36.0 h1:rIZaijrRYPeSbJG8/qNDe0hWlGrCJ7FWHNMz2SQpTis= @@ -1840,8 +1850,18 @@ github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zx github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/go-archive v0.1.0 h1:Kk/5rdW/g+H8NHdJW2gsXyZ7UnzvJNOy6VKJqueWdcQ= +github.com/moby/go-archive v0.1.0/go.mod h1:G9B+YoujNohJmrIYFBpSd54GTUB4lt9S+xVQvsJyFuo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU= github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.4.0 h1:jhcMKit7SA80hivmFJcbB1vqmw//wU61Zdui2eQXuMs= +github.com/moby/sys/user v0.4.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= +github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1854,6 +1874,8 @@ github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= github.com/montanaflynn/stats v0.7.0 h1:r3y12KyNxj/Sb/iOE46ws+3mS1+MZca1wlHQFPsY/JU= github.com/montanaflynn/stats v0.7.0/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/mtibben/percent v0.2.1 h1:5gssi8Nqo8QU/r2pynCm+hBQHpkB/uNK7BJCFogWdzs= github.com/mtibben/percent v0.2.1/go.mod h1:KG9uO+SZkUp+VkRHsCdYQV3XSZrrSpR3O9ibNBTZrns= github.com/muhlemmer/gu v0.3.1 h1:7EAqmFrW7n3hETvuAdmFmn4hS8W+z3LgKtrnow+YzNM= @@ -1954,6 +1976,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= +github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs= @@ -2033,6 +2057,9 @@ github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df h1:S77Pf5fIG github.com/shabbyrobe/gocovmerge v0.0.0-20230507112040-c3350d9342df/go.mod h1:dcuzJZ83w/SqN9k4eQqwKYMgmKWzg/KzJAURBhRL1tc= github.com/shibumi/go-pathspec v1.3.0 h1:QUyMZhFo0Md5B8zV8x2tesohbb5kfbpTi9rBnKh5dkI= github.com/shibumi/go-pathspec v1.3.0/go.mod h1:Xutfslp817l2I1cZvgcfeMQJG5QnU2lh5tVaaMCl3jE= +github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM= +github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= @@ -2116,6 +2143,10 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= +github.com/testcontainers/testcontainers-go v0.37.0 h1:L2Qc0vkTw2EHWQ08djon0D2uw7Z/PtHS/QzZZ5Ra/hg= +github.com/testcontainers/testcontainers-go v0.37.0/go.mod h1:QPzbxZhQ6Bclip9igjLFj6z0hs01bU8lrl2dHQmgFGM= +github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0 h1:LqUos1oR5iuuzorFnSvxsHNdYdCHB/DfI82CuT58wbI= +github.com/testcontainers/testcontainers-go/modules/mysql v0.37.0/go.mod h1:vHEEHx5Kf+uq5hveaVAMrTzPY8eeRZcKcl23MRw5Tkc= github.com/thales-e-security/pool v0.0.2 h1:RAPs4q2EbWsTit6tpzuvTFlgFRJ3S8Evf5gtvVDbmPg= github.com/thales-e-security/pool v0.0.2/go.mod h1:qtpMm2+thHtqhLzTwgDBj/OuNnMpupY8mv0Phz0gjhU= github.com/theupdateframework/go-tuf v0.7.0 h1:CqbQFrWo1ae3/I0UCblSbczevCCbS31Qvs5LdxRWqRI= @@ -2143,6 +2174,10 @@ github.com/tink-crypto/tink-go/v2 v2.4.0 h1:8VPZeZI4EeZ8P/vB6SIkhlStrJfivTJn+cQ4 github.com/tink-crypto/tink-go/v2 v2.4.0/go.mod h1:l//evrF2Y3MjdbpNDNGnKgCpo5zSmvUvnQ4MU+yE2sw= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26 h1:YTbkeFbzcer+42bIgo6Za2194nKwhZPgaZKsP76QffE= github.com/transparency-dev/formats v0.0.0-20250421220931-bb8ad4d07c26/go.mod h1:ODywn0gGarHMMdSkWT56ULoK8Hk71luOyRseKek9COw= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= @@ -2205,6 +2240,8 @@ github.com/yuin/goldmark-meta v1.1.0 h1:pWw+JLHGZe8Rk0EGsMVssiNb/AaPMHfSRszZeUei github.com/yuin/goldmark-meta v1.1.0/go.mod h1:U4spWENafuA7Zyg+Lj5RqK/MF+ovMYtBvXi1lBb2VP0= github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/zalando/go-keyring v0.2.3 h1:v9CUu9phlABObO4LPWycf+zwMG7nlbb3t/B5wa97yms= github.com/zalando/go-keyring v0.2.3/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk= github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= diff --git a/lib/client/db/mysql/repl/commands.go b/lib/client/db/mysql/repl/commands.go new file mode 100644 index 00000000000..7e362d36ca7 --- /dev/null +++ b/lib/client/db/mysql/repl/commands.go @@ -0,0 +1,280 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package repl + +import ( + "cmp" + "fmt" + "maps" + "slices" + "strings" + "unicode/utf8" + + "github.com/go-mysql-org/go-mysql/mysql" + "github.com/gravitational/trace" + + "github.com/gravitational/teleport" + "github.com/gravitational/teleport/lib/asciitable" +) + +type commandManager struct { + byName map[string]*command + byShortcut map[rune]*command + // shortcutPrefix is the prefix for command shortcuts, e.g. \h for help. + shortcutPrefix string +} + +// command represents a command that can be executed in the REPL. +type command struct { + // name is the command's full name. + name string + // shortcut is the command's escaped single char name: \x. + shortcut rune + // description provides a user-friendly explanation of what the command + // does. + description string + // execFunc is the function to execute the command. The commands can either + // return a reply (that will be sent back to the client) as a string. It can + // terminate the REPL by returning bool on the second argument. + execFunc func(r *REPL, args string) (reply string, exit bool) +} + +func (c *command) checkAndSetDefaults() error { + if len(c.name) == 0 { + return trace.BadParameter("missing command name") + } + if c.shortcut == 0 { + return trace.BadParameter("missing command short name") + } + if len(c.description) == 0 { + return trace.BadParameter("missing command description") + } + if c.execFunc == nil { + return trace.BadParameter("missing command exec func") + } + c.name = normalizeCommandName(c.name) + return nil +} + +func normalizeCommandName(name string) string { + return strings.ToLower(name) +} + +func newCommands() (*commandManager, error) { + c := &commandManager{ + byName: make(map[string]*command), + byShortcut: make(map[rune]*command), + shortcutPrefix: `\`, + } + for _, cmd := range []*command{ + { + name: "delimiter", + shortcut: 'd', + description: "Set statement delimiter. With no argument the delimiter resets to the default.", + execFunc: func(r *REPL, args string) (reply string, exit bool) { + delim := getArg(args) + if err := r.parser.lex.setDelimiter(delim); err != nil { + return err.Error(), false + } + return "", false + }, + }, + { + name: "help", + shortcut: 'h', + description: "Show the list of supported commands.", + execFunc: func(_ *REPL, _ string) (string, bool) { + return c.help(), false + }, + }, + { + name: "quit", + shortcut: 'q', + description: "Terminates the session.", + execFunc: func(_ *REPL, _ string) (string, bool) { return "", true }, + }, + { + name: "status", + shortcut: 's', + description: "Get status information.", + execFunc: func(r *REPL, _ string) (string, bool) { + table := asciitable.MakeHeadlessTable(2) + table.AddRow([]string{"Teleport database:", r.route.ServiceName}) + table.AddRow([]string{"Current database:", formatDatabaseName(r.route.Database)}) + table.AddRow([]string{"Current user:", r.route.Username}) + table.AddRow([]string{"Server version:", r.myConn.GetServerVersion()}) + table.AddRow([]string{"Using delimiter:", r.parser.lex.delimiter()}) + return table.String(), false + }, + }, + { + name: "teleport", + shortcut: 't', + description: "Show Teleport interactive shell information, such as execution limitations.", + execFunc: func(_ *REPL, _ string) (string, bool) { + var sb strings.Builder + for _, l := range descriptiveLimitations { + sb.WriteString("- " + strings.ReplaceAll(l, "\n", "\n ") + lineBreak) + } + return fmt.Sprintf( + "Teleport MySQL interactive shell (v%s)\n\nLimitations: \n%s", + teleport.Version, + sb.String(), + ), false + }, + }, + { + name: "use", + shortcut: 'u', + description: "Use another database.", + execFunc: func(r *REPL, args string) (string, bool) { + args = strings.TrimSpace(args) + if len(args) == 0 { + return "USE must be followed by a database name", false + } + dbName, err := getDatabaseName(r.myConn, args) + if err != nil { + return err.Error(), false + } + if err := r.myConn.UseDB(dbName); err != nil { + return err.Error(), false + } + r.route.Database = dbName + return fmt.Sprintf(`Default database changed to %q`, dbName), false + }, + }, + } { + if err := cmd.checkAndSetDefaults(); err != nil { + return nil, trace.Wrap(err) + } + if err := c.add(cmd); err != nil { + return nil, trace.Wrap(err) + } + } + return c, nil +} + +func (c *commandManager) add(cmd *command) error { + if _, ok := c.byName[cmd.name]; ok { + return trace.BadParameter("overlapping command name: %s", cmd.name) + } + c.byName[cmd.name] = cmd + if _, ok := c.byShortcut[cmd.shortcut]; ok { + return trace.BadParameter("overlapping command shortcut: %c", cmd.shortcut) + } + c.byShortcut[cmd.shortcut] = cmd + return nil +} + +// findCommand tries to find a command at the start of a line. If a command name +// or short name matches, then it returns the command and the rest of the line. +// If no command is found and the line does not start with the short name prefix +// then the command and error returned will both be nil. +func (c *commandManager) findCommand(line string) (*command, string, error) { + line = strings.TrimSpace(line) + if before, after, found := strings.Cut(line, c.shortcutPrefix); found && len(before) == 0 { + shortcut, width := utf8.DecodeRuneInString(after) + if width > 0 && shortcut != utf8.RuneError { + if cmd, ok := c.byShortcut[shortcut]; ok { + return cmd, after[1:], nil + } + } + return nil, "", trace.Errorf("Unknown command.\n%v", c.help()) + } + + name, after, _ := strings.Cut(line, " ") + if cmd, ok := c.byName[normalizeCommandName(name)]; ok { + return cmd, after, nil + } + return nil, "", nil +} + +func (c *commandManager) help() string { + var res strings.Builder + cmds := slices.Collect(maps.Values(c.byName)) + slices.SortStableFunc(cmds, func(a, b *command) int { + return strings.Compare(a.name, b.name) + }) + table := asciitable.MakeHeadlessTable(3) + for _, cmd := range cmds { + short := fmt.Sprintf("%s%c", c.shortcutPrefix, cmd.shortcut) + row := []string{cmd.name, short, cmd.description} + table.AddRow(row) + } + table.WriteTo(&res) + res.WriteString(lineBreak) + return res.String() +} + +// getArg trims spaces from the line and returns the first space separated arg. +func getArg(line string) string { + line = strings.TrimSpace(line) + before, _, _ := strings.Cut(line, " ") + return before +} + +// getDatabaseName extracts a database name from args. +// Since USE is both a command (COM_INIT_DB) and a statement, as a client +// we have to parse the database argument for COM_INIT_DB. +// To handle quotation, we send the args to the server as a statement and then +// ask the server what the current database is. +func getDatabaseName(conn mysql.Executer, args string) (string, error) { + result, err := conn.Execute("USE " + args) + if err != nil { + return "", trace.Wrap(err) + } + result.Close() + return getCurrentDatabase(conn) +} + +func getCurrentDatabase(conn mysql.Executer) (string, error) { + result, err := conn.Execute("SELECT DATABASE() LIMIT 1") + if err != nil { + return "", trace.Wrap(err) + } + defer result.Close() + if result.Resultset == nil || len(result.Resultset.Values) == 0 || len(result.Resultset.Values[0]) == 0 { + return "", trace.NotFound("missing result") + } + val := result.Resultset.Values[0][0] + if val.Type == mysql.FieldValueTypeString { + return string(val.AsString()), nil + } + return "", nil +} + +func formatDatabaseName(name string) string { + return cmp.Or(name, "(none)") +} + +// descriptiveLimitations defines a user-friendly text containing the REPL +// limitations. +var descriptiveLimitations = []string{ + `Query cancellation is not supported. Once a query is sent, its execution +can only be canceled by killing it from another session. +You can identify the query process ID with "SHOW PROCESSLIST" and then kill the +query with "KILL QUERY ". +`, + // This limitation is due to our terminal emulator not fully supporting this + // shortcut's custom handler. Instead, it will close the terminal, leading + // to terminating the session. To avoid having users accidentally + // terminating their sessions, we're turning this off until we have a better + // solution and propose the behavior for it. + // + // This shortcut filtered out by the WebUI key handler. + "Pressing CTRL-C will have no effect in this shell.", +} diff --git a/lib/client/db/mysql/repl/commands_test.go b/lib/client/db/mysql/repl/commands_test.go new file mode 100644 index 00000000000..97252d0aa91 --- /dev/null +++ b/lib/client/db/mysql/repl/commands_test.go @@ -0,0 +1,181 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package repl + +import ( + "maps" + "slices" + "testing" + + "github.com/go-mysql-org/go-mysql/mysql" + "github.com/stretchr/testify/require" + + "github.com/gravitational/teleport" + clientproto "github.com/gravitational/teleport/api/client/proto" +) + +func TestCommands(t *testing.T) { + t.Parallel() + parser, err := newParser() + require.NoError(t, err) + commands := parser.commands + require.NotEmpty(t, commands.byName) + require.ElementsMatch(t, + slices.Collect(maps.Values(commands.byName)), + slices.Collect(maps.Values(commands.byShortcut)), + ) + + for name, cmd := range commands.byName { + require.NotNil(t, cmd) + require.NotEmpty(t, cmd.name) + require.NotZero(t, cmd.shortcut) + require.NotEmpty(t, cmd.description) + require.NotNil(t, cmd.execFunc) + + require.Equal(t, cmd.name, name, "expected command to be registered by name") + byShortcut, ok := commands.byShortcut[cmd.shortcut] + require.True(t, ok) + require.Equal(t, cmd, byShortcut, "expected commands from name and shortcut indices to match") + } + + repl := &REPL{ + parser: parser, + myConn: &fakeMySQLConn{ + exec: func(command string, args ...any) (*mysql.Result, error) { + resultSet, err := mysql.BuildSimpleTextResultset([]string{"current_database"}, [][]any{{"example"}}) + if err != nil { + return nil, err + } + resultSet.Values = make([][]mysql.FieldValue, 1) + resultSet.Values[0], err = resultSet.RowDatas[0].Parse(resultSet.Fields, false, nil) + return &mysql.Result{ + Resultset: resultSet, + }, err + }, + }, + route: clientproto.RouteToDatabase{ + ServiceName: "test-service", + Username: "test-user", + Database: "test-database", + }, + } + tests := []struct { + desc string + cmdName string + args string + expectExit bool + assertCommandReply require.ValueAssertionFunc + }{ + { + desc: "delimiter command has an empty reply", + cmdName: "delimiter", + assertCommandReply: func(t require.TestingT, val any, _ ...any) { + require.Empty(t, val) + }, + }, + { + desc: "delimiter fails regex", + cmdName: "delimiter", + args: "abc", + assertCommandReply: func(t require.TestingT, val any, _ ...any) { + require.Equal(t, `DELIMITER "abc" does not match regex used for validation "^(;|[/]{2}|[$]{2})$"`, val) + }, + }, + { + desc: "help", + cmdName: "help", + assertCommandReply: func(t require.TestingT, val any, _ ...any) { + for name := range commands.byName { + require.Contains(t, val, name, `expected help command to include information about the %q command`, name) + } + }, + }, + { + desc: "quit", + cmdName: "quit", + expectExit: true, + }, + { + desc: "status", + cmdName: "status", + assertCommandReply: func(t require.TestingT, val any, _ ...any) { + require.Contains(t, val, "test-service", "expected session command to contain service name") + require.Contains(t, val, "test-user", "expected session command to contain username") + require.Contains(t, val, "test-database", "expected session command to contain database name") + }, + }, + { + desc: "teleport", + cmdName: "teleport", + assertCommandReply: func(t require.TestingT, val any, _ ...any) { + require.Contains(t, val, teleport.Version, "expected teleport command to include current Teleport version") + }, + }, + { + desc: "use requires an arg", + cmdName: "use", + assertCommandReply: func(t require.TestingT, val any, _ ...any) { + require.Equal(t, "USE must be followed by a database name", val) + }, + }, + { + desc: "use with an unquoted argument", + cmdName: "use", + args: "example", + assertCommandReply: func(t require.TestingT, val any, _ ...any) { + require.Equal(t, `Default database changed to "example"`, val) + }, + }, + { + desc: "use with a quoted argument", + cmdName: "use", + args: `"example"`, + assertCommandReply: func(t require.TestingT, val any, _ ...any) { + require.Equal(t, `Default database changed to "example"`, val) + }, + }, + } + + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + cmd, ok := commands.byName[test.cmdName] + require.True(t, ok, "expected command to be registered") + reply, exit := cmd.execFunc(repl, test.args) + if test.expectExit { + require.True(t, exit, "expected command to exit the REPL") + return + } + test.assertCommandReply(t, reply) + }) + } +} + +type fakeMySQLConn struct { + exec func(command string, args ...any) (*mysql.Result, error) +} + +func (c *fakeMySQLConn) Execute(command string, args ...any) (*mysql.Result, error) { + return c.exec(command, args...) +} + +func (c *fakeMySQLConn) UseDB(dbName string) error { + return nil +} + +func (c *fakeMySQLConn) GetServerVersion() string { + return "1.2.3 Fake MySQL Server" +} diff --git a/lib/client/db/mysql/repl/fixtures/all-data-types.sql b/lib/client/db/mysql/repl/fixtures/all-data-types.sql new file mode 100644 index 00000000000..f95614aa9c0 --- /dev/null +++ b/lib/client/db/mysql/repl/fixtures/all-data-types.sql @@ -0,0 +1,113 @@ +-- temporarily disable note warnings for test consistency +set session sql_notes = 0; +CREATE DATABASE IF NOT EXISTS all_data_types; +set session sql_notes = 1; +USE all_data_types; + +-- without ANSI_QUOTES double quotes are literals +SET SESSION sql_mode=''; +SELECT "thing"; +-- with ANSI_QUOTES double quotes are identifiers, which is what we want when +-- selecting from the table below +SET SESSION sql_mode='ANSI_QUOTES'; +SELECT "thing"; + +CREATE TABLE all_types_demo ( + "c tinyint" TINYINT, + c_smallint SMALLINT, + c_mediumint MEDIUMINT, + c_int INT, + c_bigint BIGINT, + c_decimal DECIMAL(10,2), + c_float FLOAT, + c_double DOUBLE, + c_bit BIT(8), + c_bool BOOL, + c_date DATE, + c_time TIME, + c_datetime DATETIME, + c_timestamp TIMESTAMP NULL DEFAULT NULL, + c_year YEAR, + c_char CHAR(5), + c_varchar VARCHAR(10), + c_binary BINARY(3), + c_varbinary VARBINARY(4), + c_tinyblob TINYBLOB, + c_blob BLOB, + c_mediumblob MEDIUMBLOB, + c_longblob LONGBLOB, + c_tinytext TINYTEXT, + c_text TEXT, + c_mediumtext MEDIUMTEXT, + c_longtext LONGTEXT, + c_enum ENUM('foo','bar','baz'), + c_set SET('a','b','c'), + c_geometry GEOMETRY, + c_point POINT, + c_linestring LINESTRING, + c_polygon POLYGON, + c_multipoint MULTIPOINT, + c_multilinestring MULTILINESTRING, + c_multipolygon MULTIPOLYGON, + c_geometrycoll GEOMETRYCOLLECTION, + c_json JSON +); + +INSERT INTO all_types_demo VALUES ( + 127, + 32767, + 8388607, + 2147483647, + 9223372036854775807, + 12345.67, + 3.14, + 2.7182818284, + b'10101010', + 1, + '2024-07-09', + '12:34:56', + '2024-07-09 12:34:56', + '2024-07-09 12:34:56', + 2025, + 'abcde', + 'abcdefghij', + 'abc', + 'defg', + 'a', + 'blob', + 'medblob', + 'longblob', + 'tiny', + 'text', + 'mediumtext', + 'longtext', + 'foo', + 'a,b', + ST_GeomFromText('POINT(1 1)'), + ST_GeomFromText('POINT(2 2)'), + ST_GeomFromText('LINESTRING(0 0,1 1,2 2)'), + ST_GeomFromText('POLYGON((0 0,1 0,1 1,0 1,0 0))'), + ST_GeomFromText('MULTIPOINT((0 0),(1 1))'), + ST_GeomFromText('MULTILINESTRING((0 0,1 1),(2 2,3 3))'), + ST_GeomFromText('MULTIPOLYGON(((0 0,1 0,1 1,0 1,0 0)))'), + ST_GeomFromText('GEOMETRYCOLLECTION(POINT(1 2),LINESTRING(0 0,1 1))'), + JSON_OBJECT('x', 1, 'y', 2) +); + +SELECT "c tinyint", `c_smallint`, "c_mediumint" FROM `all_types_demo`; +SELECT c_int, c_bigint, c_decimal FROM all_types_demo; +SELECT c_float, c_double, c_bit FROM all_types_demo; +SELECT c_bool, c_date, c_time FROM all_types_demo; +SELECT c_datetime, c_timestamp, c_year FROM all_types_demo; +SELECT c_char, c_varchar, c_binary FROM all_types_demo; +SELECT c_varbinary, c_tinyblob, c_blob FROM all_types_demo; +SELECT "c_mediumblob", + `c_longblob`, + c_tinytext +FROM all_types_demo; +SELECT c_text, c_mediumtext, c_longtext FROM all_types_demo; +SELECT c_enum, c_set, ST_AsText(c_geometry) AS c_geometry FROM all_types_demo; +SELECT ST_AsText(c_point) AS c_point, ST_AsText(c_linestring) AS c_linestring, ST_AsText(c_polygon) AS c_polygon FROM all_types_demo; +SELECT ST_AsText(c_multipoint) AS c_multipoint, ST_AsText(c_multilinestring) AS c_multilinestring, ST_AsText(c_multipolygon) AS c_multipolygon FROM all_types_demo; +SELECT ST_AsText(c_geometrycoll) AS c_geometrycoll, c_json FROM all_types_demo; +DROP DATABASE all_data_types; diff --git a/lib/client/db/mysql/repl/fixtures/commands.sql b/lib/client/db/mysql/repl/fixtures/commands.sql new file mode 100644 index 00000000000..1bb2e6ff8c3 --- /dev/null +++ b/lib/client/db/mysql/repl/fixtures/commands.sql @@ -0,0 +1,85 @@ +-- temporarily disable note warnings for test consistency +set session sql_notes = 0; +create database if not exists test_commands; +create database if not exists foo; +create database if not exists bar; +set session sql_notes = 1; +use test_commands; + +-- leading spaces/comments before commands +/* just a comment */ +use foo; +use foo + /* +llama + */ +use foo; + /* +llama + */ +use foo +-- llama +use foo; +-- llama +use foo + -- llama + use foo; + -- llama + use foo +/* llama */use foo; +/* llama */use foo +/* llama */ use foo; +/* llama */ use foo +/* llama */ + use foo; +/* llama */ + use foo +/* llama +*/use foo; +/* llama +*/use foo +/* llama +*/ use foo; +/* llama +*/ use foo +/* llama +-- llama +*/use foo; +/* llama +-- llama +*/use foo +/* llama +-- llama +*/ use foo; +/* llama +-- llama +*/ use foo +/* llama +-- */use foo; +/* llama +-- */use foo +/* llama +-- */ use foo; +/* llama +-- */ use foo +/* llama */ + +use foo;use bar; use foo +-- this isn't a command, but that should just print an error message and help info. +\xyz select 1 +\xyz;select 1; +delimiter $$ +help$$ +\d // +\h +help +status +\s +teleport +\t +use foo +\u mysql// +-- reset the delimiter to default ; +\d +teleport;\s;\h +use `bar`;drop database foo;drop database bar;drop database test_commands;quit diff --git a/lib/client/db/mysql/repl/fixtures/ddl.sql b/lib/client/db/mysql/repl/fixtures/ddl.sql new file mode 100644 index 00000000000..5000fcf058d --- /dev/null +++ b/lib/client/db/mysql/repl/fixtures/ddl.sql @@ -0,0 +1,92 @@ +-- temporarily disable note warnings for test consistency +set session sql_notes = 0; +DROP DATABASE IF EXISTS test_ddl_db; +CREATE DATABASE test_ddl_db; +set session sql_notes = 1; +-- this should produce a note level warning because it does exist +CREATE DATABASE if not exists test_ddl_db; +-- we clear the warning by selecting the warning count +select @@warning_count; +USE test_ddl_db; + +CREATE TABLE test_table ( + id INT PRIMARY KEY AUTO_INCREMENT, + value VARCHAR(50) NOT NULL, + note VARCHAR(100) DEFAULT NULL +) ENGINE=InnoDB; + +CREATE VIEW test_view AS ( + SELECT id, value + FROM test_table +); + +DELIMITER $$ +CREATE FUNCTION test_func(x INT) RETURNS INT DETERMINISTIC +BEGIN + RETURN x * 2; +END $$ +DELIMITER ; + +DELIMITER $$ +CREATE PROCEDURE test_proc() +BEGIN + INSERT INTO test_table(value) VALUES ('from proc'); +END $$ +DELIMITER ; + +CALL test_proc(); + +DELIMITER $$ +CREATE TRIGGER test_trigger_before_insert +BEFORE INSERT ON test_table +FOR EACH ROW +BEGIN + SET NEW.value = UPPER(NEW.value); +END $$ +DELIMITER ; + +CALL test_proc(); + +CREATE EVENT test_event + ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR + DO INSERT INTO test_table(value) VALUES ('from event'); + +set session sql_notes = 0; +-- roles are not database scoped, let's disable note warnings in case some other +-- test has already created the role or user. +CREATE ROLE IF NOT EXISTS test_role; +DROP ROLE test_role; + +CREATE USER IF NOT EXISTS test_user@'localhost' + IDENTIFIED BY 'some password' + ACCOUNT LOCK; +ALTER USER test_user@'localhost' PASSWORD EXPIRE; +DROP USER test_user@'localhost'; +set session sql_notes = 1; + +ALTER DATABASE test_ddl_db CHARACTER SET utf8mb4; +ALTER TABLE test_table ADD COLUMN note2 VARCHAR(100) DEFAULT NULL; +ALTER EVENT test_event DISABLE; +ALTER FUNCTION test_func COMMENT 'Test function'; +ALTER PROCEDURE test_proc COMMENT 'Test procedure'; + +RENAME TABLE test_table + TO test_table_renamed; +SELECT * from test_table_renamed; + +RENAME + TABLE test_table_renamed + TO test_table; + +SELECT * from test_table; + +-- there is no "test_server", this should make an error. +ALTER SERVER test_server OPTIONS (HOST '127.0.0.1'); + +DROP TRIGGER test_trigger_before_insert; + +TRUNCATE TABLE test_table ; +DROP TABLE test_table; + +DROP DATABASE test_ddl_db +; diff --git a/lib/client/db/mysql/repl/fixtures/literals.sql b/lib/client/db/mysql/repl/fixtures/literals.sql new file mode 100644 index 00000000000..9633e6c2fcb --- /dev/null +++ b/lib/client/db/mysql/repl/fixtures/literals.sql @@ -0,0 +1,50 @@ +-- with ANSI_QUOTES double quotes are identifiers +SET SESSION sql_mode='ANSI_QUOTES'; +SELECT "thing"; +-- without ANSI_QUOTES double quotes are literals +SET SESSION sql_mode=''; +SELECT "thing"; +select 1, -1,1.24, -1.24, 'foo', null, 'null'; +select + 1 as 'unsigned int', + -1 as 'negative int', + 1.24 as "float", + -1.24 as 'negative float', + 'foo' as 'string', + null as 'actual NULL', + "null" as 'string null'; + +select 1 as 'multiple rows of literals' +union + select -1 +union + select 1.24 +union + select -1.24 +union + select 'foo' +union + select null +union + select 'null'; + +select 'a +b +c' as 'multiline-string'; + +select 'a + ; + b + ; +c' as 'multiline with delimiters'; + +/* foo bar */ +select 1; +/* foo bar */select /* +baz */ 42; + +delimiter $$ +select '$$1$$'$$ +select '$$;$$';$$ +delimiter ; +select 1; diff --git a/lib/client/db/mysql/repl/lexer.go b/lib/client/db/mysql/repl/lexer.go new file mode 100644 index 00000000000..609ec62e48b --- /dev/null +++ b/lib/client/db/mysql/repl/lexer.go @@ -0,0 +1,314 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package repl + +import ( + "cmp" + "io" + "regexp" + "strings" + "unicode" + + "github.com/gravitational/trace" +) + +// lexer scans REPL input lines distinguishing between comments, strings, +// whitespace, delimiter, and normal query text. +type lexer struct { + delim string + inComment bool + inStringToken token + queryBuf strings.Builder + + lineReader strings.Reader + line string +} + +// clientDefaultStatementDelimiter is the default client-side delimiter between +// statements. This is the only delimiter that is recognized server-side, but it +// is sometimes necessary for the client to switch delimiters while typing at a +// REPL. +const clientDefaultStatementDelimiter = ";" + +// delimiter returns the current client-side delimiter. +func (l *lexer) delimiter() string { + return cmp.Or(l.delim, clientDefaultStatementDelimiter) +} + +// validDelimiterRegex only allows somewhat sane choices for delimiters. +var validDelimiterRegex = regexp.MustCompile(`^(;|[/]{2}|[$]{2})$`) + +// setDelimiter sets the client-side statement delimiter. +func (l *lexer) setDelimiter(delimiter string) error { + delimiter = strings.TrimSpace(delimiter) + if delimiter == "" { + l.delim = "" + return nil + } + if validDelimiterRegex.MatchString(delimiter) { + l.delim = delimiter + return nil + } + return trace.BadParameter("DELIMITER %q does not match regex used for validation %q", + delimiter, validDelimiterRegex.String(), + ) +} + +// writeString writes a string to the query buffer. +func (l *lexer) writeString(s string) { + l.queryBuf.WriteString(s) +} + +// writeRune writes a single rune to the query buffer. +func (l *lexer) writeRune(r rune) { + l.queryBuf.WriteRune(r) +} + +// getQuery builds the buffered query string and empties the query buffer. +func (l *lexer) getQuery() string { + out := l.queryBuf.String() + l.queryBuf.Reset() + return out +} + +// isInQuery returns true if the lexer is reading inside of a MySQL query. +func (l *lexer) isInQuery() bool { + return l.queryBuf.Len() > 0 +} + +// isInComment returns true if the lexer is reading inside of a MySQL comment. +func (l *lexer) isInComment() bool { + return l.inComment +} + +// isInString returns true if the lexer is reading inside of a MySQL string. +func (l *lexer) isInString() bool { + return len(l.inStringToken.text) > 0 +} + +// inStringKind returns the kind of MySQL string the lexer is currently reading +// inside of, i.e., the opening/closing rune: "'`. +func (l *lexer) inStringKind() string { + return l.inStringToken.text +} + +// isMultilineQuery returns true if the lexer is reading a multline query. +func (l *lexer) isMultilineQuery() bool { + return l.isInQuery() && strings.Contains(l.queryBuf.String(), lineBreak) +} + +// setCloseComment marks the start of a comment. +func (l *lexer) setOpenComment() { + l.inComment = true +} + +// setCloseComment marks the end of a comment. +func (l *lexer) setCloseComment() { + l.inComment = false +} + +// setCloseString marks the start of a MySQL string. +func (l *lexer) setOpenString(tok token) { + l.inStringToken = tok +} + +// setCloseString closes the currently open string state. +func (l *lexer) setCloseString() { + l.inStringToken = token{} +} + +// setLine sets the input line to read from. +func (l *lexer) setLine(line string) { + l.lineReader.Reset(line) + l.line = line +} + +// isEmpty returns whether the line has been completely read. +func (l *lexer) isEmpty() bool { + return l.lineReader.Len() == 0 +} + +// readRune reads a single rune and returns true if there was no error. +func (l *lexer) readRune() (rune, bool) { + r, _, err := l.lineReader.ReadRune() + ok := err == nil + return r, ok +} + +// unreadRune unreads the last rune that was read. +func (l *lexer) unreadRune() { + l.lineReader.UnreadRune() +} + +// peekString returns the remainder of the line without advancing the reader. +func (l *lexer) peekString() string { + offset := len(l.line) - l.lineReader.Len() + return l.line[offset:] +} + +// advanceByRune advances the line reader by the given rune if and only if it +// matches. +func (l *lexer) advanceByRune(want rune) bool { + if r, ok := l.readRune(); ok { + if r == want { + return true + } + l.unreadRune() + } + return false +} + +// advanceByString advances the line reader by the given string if and only if +// it is a prefix of the remaining string. +func (l *lexer) advanceByDelimiter() bool { + return l.advanceByString(l.delimiter()) +} + +// advanceByString advances the line reader by the given string if and only if +// it is a prefix of the remaining string. +func (l *lexer) advanceByString(s string) bool { + if strings.HasPrefix(l.peekString(), s) { + l.lineReader.Seek(int64(len(s)), io.SeekCurrent) + return true + } + return false +} + +// scan scans a token from the line reader. +func (l *lexer) scan() token { + r, ok := l.readRune() + if !ok { + return token{kind: tokenEOF} + } + + if unicode.IsSpace(r) { + l.unreadRune() + return token{kind: tokenSpace, text: l.readMatching(unicode.IsSpace)} + } + + switch r { + case '\\': + return token{kind: tokenBackslash, text: `\`} + case '\'': + return token{kind: tokenSingleQuote, text: "'"} + case '"': + return token{kind: tokenDoubleQuote, text: `"`} + case '`': + return token{kind: tokenBacktick, text: "`"} + case '-': + if l.advanceByRune('-') { + return token{kind: tokenSingleComment, text: "--"} + } + case '#': + return token{kind: tokenSingleComment, text: "#"} + case '/': + if l.advanceByRune('*') { + return token{kind: tokenOpenComment, text: "/*"} + } + case '*': + if l.advanceByRune('/') { + return token{kind: tokenCloseComment, text: "*/"} + } + } + return token{kind: tokenText, text: string(r)} +} + +// readMatching reads runes matching matchFn into the query buffer. +func (l *lexer) readMatching(matchFn func(rune) bool) string { + var runes []rune + for { + r, ok := l.readRune() + if !ok { + break + } + if !matchFn(r) { + l.unreadRune() + break + } + runes = append(runes, r) + } + return string(runes) +} + +// acceptEscapedRune reads the next character following an escape backslash and +// saves it to the query buffer. +func (l *lexer) acceptEscapedRune() { + if r, ok := l.readRune(); ok { + l.writeRune(r) + } +} + +// acceptString reads a MySQL string bounded by single-quotes, double-quotes, or +// backticks and saves it to the query buffer. +func (l *lexer) acceptString() { + for !l.isEmpty() { + tok := l.scan() + l.writeString(tok.text) + switch tok.kind { + case tokenBackslash: + l.acceptEscapedRune() + case l.inStringToken.kind: + // this must be a closing quote + l.setCloseString() + return + } + } +} + +// discardRemaining discards the remainder of the line. +func (l *lexer) discardRemaining() { + l.lineReader.Reset("") +} + +// discardMatching discards runes matching matchFn. +func (l *lexer) discardMatching(matchFn func(rune) bool) { + for { + r, ok := l.readRune() + if !ok { + break + } + if !matchFn(r) { + l.unreadRune() + break + } + } +} + +// discardWhitespace discards leading whitespace. +func (l *lexer) discardWhitespace() { + l.discardMatching(unicode.IsSpace) +} + +// discardSingleLineComment discards the rest of the line following a comment. +func (l *lexer) discardSingleLineComment() { + l.discardRemaining() +} + +// discardMultiLineComment discards everything inside of a multiline comment. +func (l *lexer) discardMultiLineComment() { + for !l.isEmpty() { + l.discardMatching(func(r rune) bool { + // shortcut: scan until we get a potential end of comment token + // note that escapes don't work in comments, so we don't check for them + return r != '*' + }) + if tok := l.scan(); tok.kind == tokenCloseComment { + l.setCloseComment() + return + } + } +} diff --git a/lib/client/db/mysql/repl/lexer_test.go b/lib/client/db/mysql/repl/lexer_test.go new file mode 100644 index 00000000000..9e556e98b18 --- /dev/null +++ b/lib/client/db/mysql/repl/lexer_test.go @@ -0,0 +1,298 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package repl + +import ( + "testing" + "unicode" + + "github.com/stretchr/testify/require" +) + +func TestDelimiter(t *testing.T) { + t.Parallel() + var lex lexer + require.Equal(t, clientDefaultStatementDelimiter, lex.delimiter()) + + tests := []struct { + desc string + args string + wantDelimiter string + wantErrContains string + }{ + { + desc: "passing no arg is ok", + wantDelimiter: ";", + }, + { + desc: "passing the default delimiter is ok", + args: ";", + wantDelimiter: ";", + }, + { + desc: "passing two dollars is ok", + args: "$$", + wantDelimiter: "$$", + }, + { + desc: "passing two slashes is ok", + args: "//", + wantDelimiter: "//", + }, + { + desc: "passing too many delimiter runes is invalid", + args: ";;;;;;", + wantErrContains: `DELIMITER ";;;;;;" does not match regex used for validation "^(;|[/]{2}|[$]{2})$"`, + }, + { + desc: "passing rejected runes is invalid", + args: "x", + wantErrContains: `DELIMITER "x" does not match regex used for validation "^(;|[/]{2}|[$]{2})$"`, + }, + { + desc: "passing rejected runes is invalid", + args: "\\", + wantErrContains: `DELIMITER "\\" does not match regex used for validation "^(;|[/]{2}|[$]{2})$"`, + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + var lex lexer + err := lex.setDelimiter(test.args) + if test.wantErrContains != "" { + require.ErrorContains(t, err, test.wantErrContains) + return + } + require.NoError(t, err) + require.Equal(t, test.wantDelimiter, lex.delimiter()) + }) + } +} + +func TestLexer_setLine(t *testing.T) { + t.Parallel() + var l lexer + l.setLine("SELECT * FROM t;") + require.Equal(t, "SELECT * FROM t;", l.peekString()) + require.False(t, l.isEmpty()) +} + +func TestLexer_advanceByRune(t *testing.T) { + t.Parallel() + var l lexer + l.setLine("abc") + require.True(t, l.advanceByRune('a'), "should advance on matching rune") + require.Equal(t, "bc", l.peekString()) + require.False(t, l.advanceByRune('z'), "should not advance on non-matching rune") + require.Equal(t, "bc", l.peekString()) +} + +func TestLexer_advanceByString(t *testing.T) { + t.Parallel() + var l lexer + l.setLine("foobar") + require.True(t, l.advanceByString("foo"), "should advance by matching prefix") + require.Equal(t, "bar", l.peekString()) + require.False(t, l.advanceByString("baz"), "should not advance if prefix doesn't match") + require.Equal(t, "bar", l.peekString()) +} + +func TestLexer_advanceByDelimiter(t *testing.T) { + t.Parallel() + var l lexer + l.setDelimiter(";") + l.setLine("; SELECT") + require.True(t, l.advanceByDelimiter()) + require.Equal(t, " SELECT", l.peekString()) + require.False(t, l.advanceByDelimiter()) + require.Equal(t, " SELECT", l.peekString()) + l.setLine("//") + l.setDelimiter("//") + require.True(t, l.advanceByDelimiter()) + require.Empty(t, l.peekString()) + require.False(t, l.advanceByDelimiter()) +} + +func TestLexer_getQuery(t *testing.T) { + t.Parallel() + var l lexer + l.writeString("hello") + l.writeRune(' ') + l.writeString("world!") + require.Equal(t, "hello world!", l.getQuery()) + require.Empty(t, l.getQuery(), "getQuery should reset buffer") +} + +func TestLexer_unread(t *testing.T) { + t.Parallel() + var l lexer + l.setLine("xy") + r, ok := l.readRune() + require.True(t, ok) + require.Equal(t, 'x', r) + l.unreadRune() + + r, ok = l.readRune() + require.True(t, ok) + require.Equal(t, 'x', r) + + r, ok = l.readRune() + require.True(t, ok) + require.Equal(t, 'y', r) + + _, ok = l.readRune() + require.False(t, ok, "should be false at end of input") + l.unreadRune() + _, ok = l.readRune() + require.False(t, ok, "cant unread eof") +} + +func TestLexer_readMatching(t *testing.T) { + t.Parallel() + var l lexer + l.setLine("123abcXYZ") + require.Equal(t, "123", l.readMatching(unicode.IsDigit)) + require.Equal(t, "abc", l.readMatching(unicode.IsLower)) + require.Equal(t, "XYZ", l.readMatching(unicode.IsUpper)) + require.Empty(t, l.readMatching(unicode.IsDigit)) +} + +func TestLexer_acceptEscapedRune(t *testing.T) { + t.Parallel() + var l lexer + l.setLine(`\xyz`) + tok := l.scan() + require.Equal(t, tokenBackslash, tok.kind) + l.writeString(tok.text) + l.acceptEscapedRune() + require.Equal(t, "yz", l.peekString()) + require.Equal(t, `\x`, l.getQuery()) +} + +func TestLexer_acceptString(t *testing.T) { + t.Parallel() + var l lexer + l.setLine(`"inside string "; SELECT`) + tok := l.scan() + require.Equal(t, tokenDoubleQuote, tok.kind) + l.writeString(tok.text) + l.setOpenString(tok) + l.acceptString() + require.Equal(t, "; SELECT", l.peekString()) + require.Equal(t, `"inside string "`, l.getQuery()) +} + +func TestLexer_discardRemaining(t *testing.T) { + t.Parallel() + var l lexer + l.setLine("DROP TABLE test;") + l.discardRemaining() + require.True(t, l.isEmpty()) + require.Empty(t, l.peekString()) +} + +func TestLexer_discardMatching(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input string + matchFn func(rune) bool + expected string + }{ + { + name: "discard digits", + input: "123abc", + matchFn: unicode.IsDigit, + expected: "abc", + }, + { + name: "discard lowercase", + input: "abcDEF", + matchFn: unicode.IsLower, + expected: "DEF", + }, + { + name: "nothing to discard", + input: "ABC123", + matchFn: unicode.IsLower, + expected: "ABC123", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var l lexer + l.setLine(tc.input) + l.discardMatching(tc.matchFn) + require.Equal(t, tc.expected, l.peekString()) + }) + } +} + +func TestLexer_discardWhitespace(t *testing.T) { + t.Parallel() + tests := []struct { + name string + input string + expected string + }{ + { + name: "leading whitespace only", + input: " SELECT", + expected: "SELECT", + }, + { + name: "no whitespace", + input: "SELECT", + expected: "SELECT", + }, + { + name: "whitespace and tabs", + input: "\t \n abc", + expected: "abc", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + var l lexer + l.setLine(tc.input) + l.discardWhitespace() + require.Equal(t, tc.expected, l.peekString()) + }) + } +} + +func TestLexer_discardSingleLineComment(t *testing.T) { + t.Parallel() + var l lexer + l.setLine("-- This is a comment") + l.discardSingleLineComment() + require.True(t, l.isEmpty()) + require.Empty(t, l.peekString()) +} + +func TestLexer_discardMultiLineComment(t *testing.T) { + t.Parallel() + var l lexer + l.setLine("inside comment */ SELECT 1;") + l.setOpenComment() + l.discardMultiLineComment() + require.False(t, l.isInComment(), "should have exited comment mode") + remaining := l.peekString() + require.Equal(t, " SELECT 1;", remaining) +} diff --git a/lib/client/db/mysql/repl/parser.go b/lib/client/db/mysql/repl/parser.go new file mode 100644 index 00000000000..ac64cc6f240 --- /dev/null +++ b/lib/client/db/mysql/repl/parser.go @@ -0,0 +1,195 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package repl + +import ( + "iter" + "strings" + "time" + + "github.com/gravitational/trace" +) + +func newParser() (*parser, error) { + commands, err := newCommands() + if err != nil { + return nil, trace.Wrap(err) + } + return &parser{commands: commands}, nil +} + +// parser parses commands and queries given one or more lines of input. +type parser struct { + commands *commandManager + lex lexer +} + +// parse returns an iterator over the input line yielding evaluators for each +// command or query found. +func (p *parser) parse(line string) iter.Seq[evaluator] { + p.lex.setLine(line) + return func(yield func(evaluator) bool) { + defer func() { + if p.lex.isEmpty() && p.lex.isInQuery() { + // must be a multiline query, so insert a linebreak in the query buf + p.lex.writeString(lineBreak) + } + }() + + for !p.lex.isEmpty() { + if p.lex.isInComment() { + // dont preserve comments + p.lex.discardMultiLineComment() + continue + } + + if !p.lex.isInQuery() { + // not in a comment or query: is the rest of the line a command? + if cmd := p.tryParseCommand(); cmd != nil { + if !yield(cmd) { + return + } + continue + } + } + + if p.lex.isInString() { + p.lex.acceptString() + continue + } + + // not in comment nor string: do we have a command/query delimiter? + if cmdOrQuery := p.tryParseQuery(); cmdOrQuery != nil { + if !yield(cmdOrQuery) { + return + } + continue + } + + // not in comment nor string and no delimiter found: scan text + tok := p.lex.scan() + switch tok.kind { + case tokenSingleComment: + // dont preserve comments + p.lex.discardSingleLineComment() + case tokenOpenComment: + p.lex.setOpenComment() + p.lex.discardMultiLineComment() + case tokenBackslash: + p.lex.writeString(tok.text) + p.lex.acceptEscapedRune() + case tokenSingleQuote, tokenDoubleQuote, tokenBacktick: + p.lex.writeString(tok.text) + p.lex.setOpenString(tok) + p.lex.acceptString() + default: + p.lex.writeString(tok.text) + } + } + } +} + +// tryParseCommand tries to parse a command and its args on the current line. +// If the line contains the current delimiter and the command is not the special +// DELIMITER command, then give up and let the query parser handle it. +func (p *parser) tryParseCommand() evaluator { + p.lex.discardWhitespace() + line := p.lex.peekString() + cmd, args, err := p.commands.findCommand(line) + switch { + case err != nil: + if strings.Contains(line, p.lex.delimiter()) { + return nil + } + p.lex.discardRemaining() + return errorEvaluator{err: err} + case cmd != nil: + if cmd.name != "delimiter" && strings.Contains(line, p.lex.delimiter()) { + return nil + } + p.lex.discardRemaining() + return commandEvaluator{cmd: cmd, args: args} + default: + return nil + } +} + +// tryParseQuery looks for a delimiter in the input. If it finds one and the +// query buffer is all from one line, then it first looks for a command in the +// query buffer. If there is no command or the query buffer is multiline, then +// it returns a query evaluator. +func (p *parser) tryParseQuery() evaluator { + if p.lex.advanceByDelimiter() { + query := p.lex.getQuery() + if !p.lex.isMultilineQuery() || strings.HasPrefix(query, p.commands.shortcutPrefix) { + cmd, args, err := p.commands.findCommand(query) + switch { + case err != nil: + // don't discard the rest of the line, since we found the + // command in the query buffer, unlike the command parser which + // looks ahead in the current line for a command + return errorEvaluator{err: err} + case cmd != nil: + return commandEvaluator{cmd: cmd, args: args} + } + } + return queryEvaluator(query) + } + return nil +} + +type evaluator interface { + eval(r *REPL) (reply string, exit bool) +} + +// queryEvaluator executes a single query and formats the response as a reply +// to the REPL client. +type queryEvaluator string + +func (query queryEvaluator) eval(r *REPL) (string, bool) { + start := time.Now() + result, err := r.myConn.Execute(string(query)) + if err != nil { + return errorReplyPrefix + err.Error(), false + } + defer result.Close() + if r.disableQueryTimings { + return formatResult(result, nil), false + } + + elapsed := time.Since(start) + return formatResult(result, &elapsed), false +} + +// commandEvaluator executes a command. +type commandEvaluator struct { + cmd *command + args string +} + +func (e commandEvaluator) eval(r *REPL) (string, bool) { + return e.cmd.execFunc(r, e.args) +} + +type errorEvaluator struct { + err error +} + +// errorEvaluator formats an error as a reply to the REPL client. +func (e errorEvaluator) eval(_ *REPL) (string, bool) { + return e.err.Error(), false +} diff --git a/lib/client/db/mysql/repl/repl.go b/lib/client/db/mysql/repl/repl.go new file mode 100644 index 00000000000..018c12bad07 --- /dev/null +++ b/lib/client/db/mysql/repl/repl.go @@ -0,0 +1,281 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package repl + +import ( + "cmp" + "context" + "errors" + "fmt" + "io" + "iter" + "net" + "strings" + "time" + + "github.com/go-mysql-org/go-mysql/client" + "github.com/go-mysql-org/go-mysql/mysql" + "github.com/gravitational/trace" + "golang.org/x/term" + + "github.com/gravitational/teleport" + clientproto "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/lib/asciitable" + dbrepl "github.com/gravitational/teleport/lib/client/db/repl" +) + +// REPL implements [dbrepl.REPLInstance] for MySQL. +type REPL struct { + client io.ReadWriteCloser + serverConn net.Conn + route clientproto.RouteToDatabase + term *term.Terminal + parser *parser + myConn mysqlConn + + // teleportVersion is used in golden tests to fake the current Teleport + // version and prevent test failures when the real version is incremented. + teleportVersion string + + // testPassword is normally blank, only used in tests where the REPL connects + // directly to a MySQL instance without Teleport proxying. + testPassword string + // disableQueryTimings is used in golden tests to disable query timings for + // test consistency. + disableQueryTimings bool +} + +type mysqlConn interface { + Execute(command string, args ...any) (*mysql.Result, error) + UseDB(dbName string) error + GetServerVersion() string +} + +// New implements [dbrepl.REPLNewFunc]. +func New(_ context.Context, cfg *dbrepl.NewREPLConfig) (dbrepl.REPLInstance, error) { + parser, err := newParser() + if err != nil { + return nil, trace.Wrap(err) + } + return &REPL{ + client: cfg.Client, + serverConn: cfg.ServerConn, + route: cfg.Route, + term: term.NewTerminal(cfg.Client, ""), + parser: parser, + }, nil +} + +// Run starts and run the PostgreSQL REPL session. The provided context is used +// to interrupt the execution and clean up resources. +func (r *REPL) Run(ctx context.Context) error { + dialer := func(context.Context, string, string) (net.Conn, error) { + return r.serverConn, nil + } + const hostnamePlaceholder = "repl" + myConn, err := client.ConnectWithDialer(ctx, "tcp", hostnamePlaceholder, + r.route.GetUsername(), + r.testPassword, + r.route.GetDatabase(), + dialer, + withClientCapabilities( + mysql.CLIENT_MULTI_RESULTS, + mysql.CLIENT_MULTI_STATEMENTS, + ), + func(c *client.Conn) error { + c.SetAttributes(map[string]string{clientNameParamName: clientNameParamValue}) + return nil + }, + ) + if err != nil { + return trace.ConnectionProblem(err, "Unable to connect to database: %v", err) + } + r.myConn = myConn + defer func() { + _ = myConn.Close() + }() + + // term.Terminal blocks reads/writes without respecting the context. The + // only thing that unblocks it is closing the underlaying connection (in + // our case r.client). On this goroutine we only watch for context + // cancelation and close the connection. This will unblocks all terminal + // reads/writes. + stop := context.AfterFunc(ctx, func() { + _ = r.client.Close() + }) + defer stop() + + if err := r.presentBanner(); err != nil { + return trace.Wrap(err) + } + + for { + line, err := r.readLine() + if err != nil { + return trace.Wrap(rewriteTermError(ctx, err)) + } + for reply, exit := range r.eval(line) { + if exit { + return nil + } + if err := r.print(reply); err != nil { + return trace.Wrap(err) + } + } + } +} + +func withClientCapabilities(caps ...uint32) client.Option { + return func(conn *client.Conn) error { + for _, cap := range caps { + conn.SetCapability(cap) + } + return nil + } +} + +func (r *REPL) presentBanner() error { + _, err := fmt.Fprintf( + r.term, + `Teleport MySQL interactive shell (v%s) +Connected to instance %q as user %q. +Type 'help' or '\h' for help. + +`, + cmp.Or(r.teleportVersion, teleport.Version), + r.route.GetServiceName(), + r.route.GetUsername()) + return trace.Wrap(err) +} + +func (r *REPL) readLine() (string, error) { + r.term.SetPrompt(r.getPrompt()) + return r.term.ReadLine() +} + +func (r *REPL) getPrompt() string { + dbName := formatDatabaseName(r.route.Database) + var suffix string + switch { + case r.parser.lex.isInComment(): + suffix = "/*" + case r.parser.lex.isInString(): + suffix = r.parser.lex.inStringKind() + case r.parser.lex.isInQuery(): + suffix = "-" + default: + suffix = dbName + } + padding := len(dbName) + // pad the suffix with leading spaces until it is len(dbName), for example: + // default: "mysql>" + // comment: " */>" + // string: " '>" + // query: " ->" + return fmt.Sprintf("%*s> ", padding, suffix) +} + +func (r *REPL) eval(line string) iter.Seq2[string, bool] { + return func(yield func(string, bool) bool) { + for evaluator := range r.parser.parse(line) { + if !yield(evaluator.eval(r)) { + return + } + } + } +} + +// print writes a reply to the REPL client. +func (r *REPL) print(reply string) error { + reply = strings.TrimSpace(reply) + if reply == "" { + return nil + } + _, err := r.term.Write([]byte(lineBreak + reply + lineBreak + lineBreak)) + return trace.Wrap(err) +} + +// rewriteTermError changes the term.Terminal error to match caller expectations. +func rewriteTermError(ctx context.Context, err error) error { + // When context is canceled it will immediately lead read/write errors due + // to the closed connection. For this cases we return the context error. + if ctx.Err() != nil && (errors.Is(err, io.ErrClosedPipe) || errors.Is(err, net.ErrClosed)) { + return ctx.Err() + } + if errors.Is(err, io.EOF) { + return nil + } + + return err +} + +// formatResult formats a query result. +func formatResult(result *mysql.Result, elapsed *time.Duration) string { + var out string + if result.Resultset != nil { + out = formatResultset(result.Resultset) + } else { + out = fmt.Sprintf("%v warnings, %v rows affected", result.Warnings, result.AffectedRows) + } + if elapsed != nil { + return fmt.Sprintf("%s (%.2f sec)", out, elapsed.Seconds()) + } + return out +} + +// formatResultset formats a query result that contains rows. +func formatResultset(set *mysql.Resultset) string { + if set == nil || len(set.Values) == 0 { + return "Empty set" + } + var sb strings.Builder + columns := make([]string, 0, len(set.Fields)) + for _, f := range set.Fields { + columns = append(columns, string(f.Name)) + } + table := asciitable.MakeTable(columns) + for _, rowValues := range set.Values { + row := make([]string, 0, len(rowValues)) + for _, val := range rowValues { + row = append(row, val.String()) + } + table.AddRow(row) + } + table.WriteTo(&sb) + sb.WriteString(lineBreak) + + if numRows := len(set.Values); numRows == 1 { + sb.WriteString("1 row in set") + } else { + // pluralize 0 or multiple rows + fmt.Fprintf(&sb, "%d rows in set", numRows) + } + return sb.String() +} + +const ( + // lineBreak represents a line break on the REPL. + lineBreak = "\n" + // errorReplyPrefix is the prefix presented when there is a execution error. + errorReplyPrefix = "ERR " + // clientNameParamName defines the application name parameter name. + // + // https://dev.mysql.com/doc/refman/8.0/en/performance-schema-connection-attribute-tables.html + clientNameParamName = "_client_name" + // clientNameParamValue defines the application name parameter value. + clientNameParamValue = "teleport-repl" +) diff --git a/lib/client/db/mysql/repl/repl_test.go b/lib/client/db/mysql/repl/repl_test.go new file mode 100644 index 00000000000..3716fe0e70f --- /dev/null +++ b/lib/client/db/mysql/repl/repl_test.go @@ -0,0 +1,455 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package repl + +import ( + "bytes" + "cmp" + "context" + "crypto/rand" + _ "embed" + "io" + "net" + "os" + "strings" + "sync" + "testing" + "time" + + "github.com/go-mysql-org/go-mysql/mysql" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + mysqlcontainer "github.com/testcontainers/testcontainers-go/modules/mysql" + "golang.org/x/term" + + clientproto "github.com/gravitational/teleport/api/client/proto" + dbrepl "github.com/gravitational/teleport/lib/client/db/repl" + "github.com/gravitational/teleport/lib/defaults" + "github.com/gravitational/teleport/lib/utils/testutils/golden" +) + +var testSrv *testServer +var testSrvMu sync.Mutex + +func TestMain(m *testing.M) { + code := m.Run() + + // The "Ryuk" container will cleanup everything anyhow, but only after 10s + // without any client connections. + // We can speed up container cleanup after a -count=N run, e.g., a flaky + // test detector run, by terminating them immediately after tests have run. + // https://github.com/testcontainers/moby-ryuk + testSrvMu.Lock() + defer testSrvMu.Unlock() + // if we don't clean up after 10 seconds, RYUK will get them anyway. + ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) + defer cancel() + + // ignore errors, Ryuk or worst case VM termination in CI will deal with it + _ = testSrv.container.Terminate(ctx) + os.Exit(code) +} + +func TestREPL(t *testing.T) { + testSrv := newTestServer(t) + route := clientproto.RouteToDatabase{ + ServiceName: "mysql-test-container", + Protocol: defaults.ProtocolMySQL, + Username: testSrv.username, + // Pass a blank database name, as if a user who didn't specify one. + // The REPL should handle this case gracefully. + Database: "", + } + for _, test := range []struct { + name string + input string + }{ + { + name: "ddl", + input: testDDL, + }, + { + name: "all data types", + input: testDataTypes, + }, + { + name: "literals", + input: testLiterals, + }, + { + name: "commands", + input: testCommands, + }, + } { + t.Run(test.name, func(t *testing.T) { + // the term only recognizes line breaks with a carriage-return + const crlf = "\r\n" + input := strings.ReplaceAll(test.input, "\n", crlf) + crlf + recorder := newRecordingClient(t, io.NopCloser(strings.NewReader(input))) + testREPL, err := New(t.Context(), &dbrepl.NewREPLConfig{ + Client: recorder, + ServerConn: testSrv.connectTCP(t, 10*time.Second), + Route: route, + }) + require.NoError(t, err) + testREPL.(*REPL).testPassword = testSrv.password + testREPL.(*REPL).disableQueryTimings = true + err = testREPL.Run(t.Context()) + require.NoError(t, err) + goldenName := strings.ReplaceAll(test.name, " ", "-") + if golden.ShouldSet() { + golden.SetNamed(t, goldenName, recorder.buf.Bytes()) + } + require.Equal(t, string(golden.GetNamed(t, goldenName)), recorder.buf.String()) + }) + } +} + +var ( + //go:embed fixtures/ddl.sql + testDDL string + //go:embed fixtures/all-data-types.sql + testDataTypes string + //go:embed fixtures/literals.sql + testLiterals string + //go:embed fixtures/commands.sql + testCommands string +) + +func TestGetPrompt(t *testing.T) { + t.Parallel() + tests := []struct { + desc string + inQuery bool + inComment bool + inStringKind string + database string + want string + }{ + { + desc: "no default database", + want: "(none)> ", + }, + { + desc: "default", + database: "llama", + want: "llama> ", + }, + { + desc: "in a query", + inQuery: true, + database: "llama", + want: " -> ", + }, + { + desc: "in a comment", + inComment: true, + database: "llama", + want: " /*> ", + }, + { + desc: "in a quote string", + inStringKind: "'", + database: "llama", + want: " '> ", + }, + { + desc: "in a double quote string", + inStringKind: `"`, + database: "llama", + want: ` "> `, + }, + { + desc: "in a backtick string", + inStringKind: "`", + database: "llama", + want: " `> ", + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + r := &REPL{ + route: clientproto.RouteToDatabase{ + Database: test.database, + }, + parser: &parser{ + lex: lexer{ + inComment: test.inComment, + inStringToken: token{text: test.inStringKind}, + }, + }, + teleportVersion: "v19.0.0-dev", + } + if test.inQuery { + r.parser.lex.queryBuf.WriteString("select") + } + require.Equal(t, test.want, r.getPrompt()) + }) + } +} + +func FuzzEval(f *testing.F) { + parser, err := newParser() + require.NoError(f, err) + + for _, cmd := range parser.commands.byName { + f.Add(cmd.name) + f.Add(parser.commands.shortcutPrefix + string(cmd.shortcut)) + } + + f.Add("") // eof + f.Add(" ") // space + + // MySQL string literal quotes + f.Add("'") + f.Add(`"`) + f.Add("`") + + // single line comments + f.Add("--") + f.Add("#") + + // multiline comments + f.Add("/*") + f.Add("*/") + f.Add("*") + + // valid delimiters + f.Add(";") + f.Add("$") + f.Add("/") + + // some statement syntax + f.Add("select") + f.Add("create") + f.Add("drop") + f.Add("replace") + f.Add("alter") + f.Add("(") + f.Add(")") + + // multiline + f.Add("\n") + + f.Fuzz(func(t *testing.T, line string) { + repl := &REPL{ + myConn: &fakeMySQLConn{ + exec: func(command string, args ...any) (*mysql.Result, error) { + return nil, io.EOF + }, + }, + parser: parser, + } + require.NotPanics(t, func() { + for line := range strings.SplitSeq(line, "\n") { + for _, exit := range repl.eval(line) { + if exit { + return + } + } + } + }) + }) +} + +func TestInteractively(t *testing.T) { + // You can get an interactive REPL to experiment with like this: + // $ export MYSQL_TEST_REPL_INTERACTIVELY=1 + // $ go test -c -o ./test ./lib/client/db/mysql/repl + // $ ./test -test.v -test.count=1 -test.run TestInteractively + if os.Getenv("MYSQL_TEST_REPL_INTERACTIVELY") == "" { + t.Skip() + } + testSrv := newTestServer(t) + route := clientproto.RouteToDatabase{ + ServiceName: "mysql-test-container", + Protocol: defaults.ProtocolMySQL, + Username: testSrv.username, + Database: testSrv.database, + Roles: []string{}, + } + oldState, err := term.MakeRaw(int(os.Stdin.Fd())) + require.NoError(t, err) + defer term.Restore(int(os.Stdin.Fd()), oldState) + recorder := newRecordingClient(t, os.Stdin, os.Stdout) + repl, err := New(t.Context(), &dbrepl.NewREPLConfig{ + Client: recorder, + ServerConn: testSrv.connectTCP(t, 10*time.Second), + Route: route, + }) + require.NoError(t, err) + repl.(*REPL).testPassword = testSrv.password + require.NoError(t, repl.Run(t.Context())) +} + +func Test_formatResult(t *testing.T) { + resultRows, err := mysql.BuildSimpleTextResultset( + []string{"one", "two", "three"}, + [][]any{{1, 2.2, "3"}, {4, 5.5, "6"}, {7, 8.8, "8"}}, + ) + require.NoError(t, err) + + noRows, err := mysql.BuildSimpleTextResultset( + []string{"one", "two", "three"}, + nil, + ) + require.NoError(t, err) + + tests := []struct { + desc string + input *mysql.Result + elapsed time.Duration + }{ + { + desc: "result rows", + input: &mysql.Result{Resultset: resultRows}, + elapsed: time.Millisecond * 1234, + }, + { + desc: "long elapsed time", + input: &mysql.Result{Resultset: resultRows}, + elapsed: time.Millisecond * 123456, + }, + { + desc: "empty set", + input: &mysql.Result{Resultset: noRows}, + elapsed: time.Millisecond * 42, + }, + { + desc: "nil set", + input: &mysql.Result{Warnings: 2, AffectedRows: 42}, + elapsed: 0, + }, + } + for _, test := range tests { + t.Run(test.desc, func(t *testing.T) { + mustParseResult(t, test.input) + got := formatResult(test.input, &test.elapsed) + goldenName := strings.ReplaceAll(test.desc, " ", "-") + if golden.ShouldSet() { + golden.SetNamed(t, goldenName, []byte(got)) + } + require.Equal(t, string(golden.GetNamed(t, goldenName)), got) + }) + } +} + +func mustParseResult(t *testing.T, result *mysql.Result) { + t.Helper() + if result.Resultset == nil { + return + } + result.Values = make([][]mysql.FieldValue, len(result.RowDatas)) + for i := range result.RowDatas { + const isBinary = false + v, err := result.RowDatas[i].Parse(result.Fields, isBinary, result.Values[i]) + result.Values[i] = v + require.NoError(t, err) + } +} + +func newTestServer(t *testing.T) *testServer { + t.Helper() + ctx := t.Context() + + reuseName := cmp.Or(os.Getenv("MYSQL_TEST_SERVER_REUSE_CONTAINER_BY_NAME"), "default-mysql-test-server") + testSrvMu.Lock() + // hold the lock for the entire func to avoid parallel container requests + defer testSrvMu.Unlock() + if testSrv != nil { + return testSrv + } + + user := cmp.Or(os.Getenv("MYSQL_TEST_SERVER_USER"), "root") + db := cmp.Or(os.Getenv("MYSQL_TEST_SERVER_DB"), "mysql") + pass := cmp.Or(os.Getenv("MYSQL_TEST_SERVER_PASS"), rand.Text()) + opts := []testcontainers.ContainerCustomizer{ + mysqlcontainer.WithDatabase(db), + mysqlcontainer.WithUsername(user), + mysqlcontainer.WithPassword(pass), + testcontainers.WithReuseByName(reuseName), + } + + // MySQL 8.4.6 index digest + // https://docs.docker.com/dhi/core-concepts/digests/#multi-platform-images-and-manifests + // https://hub.docker.com/layers/library/mysql/8.4.6/images/sha256-9b9413211d004062e6d5be1118e1051773fe66d832ec9ebcae1c2fcce5af5f5b + const img = "mysql@sha256:d2c60b1b225c6d7845f0abdb596fc35c2d4122bcad6ec219588035a118f75d93" + container, err := mysqlcontainer.Run(ctx, img, opts...) + require.NoError(t, err) + + host, err := container.Host(ctx) + require.NoError(t, err) + + mappedPort, err := container.MappedPort(ctx, "3306/tcp") + require.NoError(t, err) + + srv := &testServer{ + container: container, + host: host, + port: mappedPort.Port(), + username: user, + password: pass, + database: db, + } + testSrv = srv + return srv +} + +// testServer is a MySQL server for tests. +type testServer struct { + // container is the container hosting the server. + container testcontainers.Container + // host is the MySQL connection endpoint host. + host string + // port is the MySQL connection endpoint port. + port string + // username is the database superuser. + username string + // password is the database superuser password. + password string + // database is a database schema provisioned in the database. + database string +} + +// hostPort returns the server host:port. +func (s *testServer) hostPort() string { + return net.JoinHostPort(s.host, s.port) +} + +func (s *testServer) connectTCP(t *testing.T, timeout time.Duration) net.Conn { + t.Helper() + conn, err := net.DialTimeout("tcp", s.hostPort(), timeout) + require.NoError(t, err) + return conn +} + +func newRecordingClient(t *testing.T, reader io.ReadCloser, writers ...io.Writer) *recordingClient { + t.Helper() + var buf bytes.Buffer + writers = append(writers, &buf) + return &recordingClient{ + ReadCloser: reader, + Writer: io.MultiWriter(writers...), + buf: &buf, + } +} + +type recordingClient struct { + io.ReadCloser + io.Writer + buf *bytes.Buffer +} diff --git a/lib/client/db/mysql/repl/testdata/TestREPL/all_data_types/all-data-types.golden b/lib/client/db/mysql/repl/testdata/TestREPL/all_data_types/all-data-types.golden new file mode 100644 index 00000000000..7c1d023c19a --- /dev/null +++ b/lib/client/db/mysql/repl/testdata/TestREPL/all_data_types/all-data-types.golden @@ -0,0 +1,255 @@ +Teleport MySQL interactive shell (v19.0.0-dev) +Connected to instance "mysql-test-container" as user "root". +Type 'help' or '\h' for help. + +(none)> -- temporarily disable note warnings for test consistency +(none)> set session sql_notes = 0; + +0 warnings, 0 rows affected + +(none)> CREATE DATABASE IF NOT EXISTS all_data_types; + +0 warnings, 1 rows affected + +(none)> set session sql_notes = 1; + +0 warnings, 0 rows affected + +(none)> USE all_data_types; + +Default database changed to "all_data_types" + +all_data_types> +all_data_types> -- without ANSI_QUOTES double quotes are literals +all_data_types> SET SESSION sql_mode=''; + +0 warnings, 0 rows affected + +all_data_types> SELECT "thing"; + +thing +------- +'thing' + +1 row in set + +all_data_types> -- with ANSI_QUOTES double quotes are identifiers, which is what + we want when +all_data_types> -- selecting from the table below +all_data_types> SET SESSION sql_mode='ANSI_QUOTES'; + +0 warnings, 0 rows affected + +all_data_types> SELECT "thing"; + +ERR ERROR 1054 (42S22): Unknown column 'thing' in 'field list' + +all_data_types> +all_data_types> CREATE TABLE all_types_demo ( + -> "c tinyint" TINYINT, + -> c_smallint SMALLINT, + -> c_mediumint MEDIUMINT, + -> c_int INT, + -> c_bigint BIGINT, + -> c_decimal DECIMAL(10,2), + -> c_float FLOAT, + -> c_double DOUBLE, + -> c_bit BIT(8), + -> c_bool BOOL, + -> c_date DATE, + -> c_time TIME, + -> c_datetime DATETIME, + -> c_timestamp TIMESTAMP NULL DEFAULT NULL, + -> c_year YEAR, + -> c_char CHAR(5), + -> c_varchar VARCHAR(10), + -> c_binary BINARY(3), + -> c_varbinary VARBINARY(4), + -> c_tinyblob TINYBLOB, + -> c_blob BLOB, + -> c_mediumblob MEDIUMBLOB, + -> c_longblob LONGBLOB, + -> c_tinytext TINYTEXT, + -> c_text TEXT, + -> c_mediumtext MEDIUMTEXT, + -> c_longtext LONGTEXT, + -> c_enum ENUM('foo','bar','baz'), + -> c_set SET('a','b','c'), + -> c_geometry GEOMETRY, + -> c_point POINT, + -> c_linestring LINESTRING, + -> c_polygon POLYGON, + -> c_multipoint MULTIPOINT, + -> c_multilinestring MULTILINESTRING, + -> c_multipolygon MULTIPOLYGON, + -> c_geometrycoll GEOMETRYCOLLECTION, + -> c_json JSON + -> ); + +0 warnings, 0 rows affected + +all_data_types> +all_data_types> INSERT INTO all_types_demo VALUES ( + -> 127, + -> 32767, + -> 8388607, + -> 2147483647, + -> 9223372036854775807, + -> 12345.67, + -> 3.14, + -> 2.7182818284, + -> b'10101010', + -> 1, + -> '2024-07-09', + -> '12:34:56', + -> '2024-07-09 12:34:56', + -> '2024-07-09 12:34:56', + -> 2025, + -> 'abcde', + -> 'abcdefghij', + -> 'abc', + -> 'defg', + -> 'a', + -> 'blob', + -> 'medblob', + -> 'longblob', + -> 'tiny', + -> 'text', + -> 'mediumtext', + -> 'longtext', + -> 'foo', + -> 'a,b', + -> ST_GeomFromText('POINT(1 1)'), + -> ST_GeomFromText('POINT(2 2)'), + -> ST_GeomFromText('LINESTRING(0 0,1 1,2 2)'), + -> ST_GeomFromText('POLYGON((0 0,1 0,1 1,0 1,0 0))'), + -> ST_GeomFromText('MULTIPOINT((0 0),(1 1))'), + -> ST_GeomFromText('MULTILINESTRING((0 0,1 1),(2 2,3 3))'), + -> ST_GeomFromText('MULTIPOLYGON(((0 0,1 0,1 1,0 1,0 0)))'), + -> ST_GeomFromText('GEOMETRYCOLLECTION(POINT(1 2),LINESTRING(0 +0,1 1))'), + -> JSON_OBJECT('x', 1, 'y', 2) + -> ); + +0 warnings, 1 rows affected + +all_data_types> +all_data_types> SELECT "c tinyint", `c_smallint`, "c_mediumint" FROM `all_types_ +demo`; + +c tinyint c_smallint c_mediumint +--------- ---------- ----------- +127 32767 8388607 + +1 row in set + +all_data_types> SELECT c_int, c_bigint, c_decimal FROM all_types_demo; + +c_int c_bigint c_decimal +---------- ------------------- ---------- +2147483647 9223372036854775807 '12345.67' + +1 row in set + +all_data_types> SELECT c_float, c_double, c_bit FROM all_types_demo; + +c_float c_double c_bit +------- ------------ ----- +3.14 2.7182818284 'ª' + +1 row in set + +all_data_types> SELECT c_bool, c_date, c_time FROM all_types_demo; + +c_bool c_date c_time +------ ------------ ---------- +1 '2024-07-09' '12:34:56' + +1 row in set + +all_data_types> SELECT c_datetime, c_timestamp, c_year FROM all_types_demo; + +c_datetime c_timestamp c_year +--------------------- --------------------- ------ +'2024-07-09 12:34:56' '2024-07-09 12:34:56' 2025 + +1 row in set + +all_data_types> SELECT c_char, c_varchar, c_binary FROM all_types_demo; + +c_char c_varchar c_binary +------- ------------ -------- +'abcde' 'abcdefghij' 'abc' + +1 row in set + +all_data_types> SELECT c_varbinary, c_tinyblob, c_blob FROM all_types_demo; + +c_varbinary c_tinyblob c_blob +----------- ---------- ------ +'defg' 'a' 'blob' + +1 row in set + +all_data_types> SELECT "c_mediumblob", + -> `c_longblob`, + -> c_tinytext + -> FROM all_types_demo; + +c_mediumblob c_longblob c_tinytext +------------ ---------- ---------- +'medblob' 'longblob' 'tiny' + +1 row in set + +all_data_types> SELECT c_text, c_mediumtext, c_longtext FROM all_types_demo; + +c_text c_mediumtext c_longtext +------ ------------ ---------- +'text' 'mediumtext' 'longtext' + +1 row in set + +all_data_types> SELECT c_enum, c_set, ST_AsText(c_geometry) AS c_geometry FROM a +ll_types_demo; + +c_enum c_set c_geometry +------ ----- ------------ +'foo' 'a,b' 'POINT(1 1)' + +1 row in set + +all_data_types> SELECT ST_AsText(c_point) AS c_point, ST_AsText(c_linestring) AS + c_linestring, ST_AsText(c_polygon) AS c_polygon FROM all_types_demo; + +c_point c_linestring c_polygon +------------ ------------------------- -------------------------------- +'POINT(2 2)' 'LINESTRING(0 0,1 1,2 2)' 'POLYGON((0 0,1 0,1 1,0 1,0 0))' + +1 row in set + +all_data_types> SELECT ST_AsText(c_multipoint) AS c_multipoint, ST_AsText(c_mult +ilinestring) AS c_multilinestring, ST_AsText(c_multipolygon) AS c_multipolygon F +ROM all_types_demo; + +c_multipoint c_multilinestring c_multipolygon +------------------------- -------------------------------------- --------------------------------------- +'MULTIPOINT((0 0),(1 1))' 'MULTILINESTRING((0 0,1 1),(2 2,3 3))' 'MULTIPOLYGON(((0 0,1 0,1 1,0 1,0 0)))' + +1 row in set + +all_data_types> SELECT ST_AsText(c_geometrycoll) AS c_geometrycoll, c_json FROM +all_types_demo; + +c_geometrycoll c_json +---------------------------------------------------- ------------------ +'GEOMETRYCOLLECTION(POINT(1 2),LINESTRING(0 0,1 1))' '{"x": 1, "y": 2}' + +1 row in set + +all_data_types> DROP DATABASE all_data_types; + +0 warnings, 1 rows affected + +all_data_types> +all_data_types> \ No newline at end of file diff --git a/lib/client/db/mysql/repl/testdata/TestREPL/commands/commands.golden b/lib/client/db/mysql/repl/testdata/TestREPL/commands/commands.golden new file mode 100644 index 00000000000..130a589162c --- /dev/null +++ b/lib/client/db/mysql/repl/testdata/TestREPL/commands/commands.golden @@ -0,0 +1,325 @@ +Teleport MySQL interactive shell (v19.0.0-dev) +Connected to instance "mysql-test-container" as user "root". +Type 'help' or '\h' for help. + +(none)> -- temporarily disable note warnings for test consistency +(none)> set session sql_notes = 0; + +0 warnings, 0 rows affected + +(none)> create database if not exists test_commands; + +0 warnings, 1 rows affected + +(none)> create database if not exists foo; + +0 warnings, 1 rows affected + +(none)> create database if not exists bar; + +0 warnings, 1 rows affected + +(none)> set session sql_notes = 1; + +0 warnings, 0 rows affected + +(none)> use test_commands; + +Default database changed to "test_commands" + +test_commands> +test_commands> -- leading spaces/comments before commands +test_commands> /* just a comment */ +test_commands> use foo; + +Default database changed to "foo" + +foo> use foo + +Default database changed to "foo" + +foo> /* + /*> llama + /*> */ +foo> use foo; + +Default database changed to "foo" + +foo> /* + /*> llama + /*> */ +foo> use foo + +Default database changed to "foo" + +foo> -- llama +foo> use foo; + +Default database changed to "foo" + +foo> -- llama +foo> use foo + +Default database changed to "foo" + +foo> -- llama +foo> use foo; + +Default database changed to "foo" + +foo> -- llama +foo> use foo + +Default database changed to "foo" + +foo> /* llama */use foo; + +Default database changed to "foo" + +foo> /* llama */use foo + +Default database changed to "foo" + +foo> /* llama */ use foo; + +Default database changed to "foo" + +foo> /* llama */ use foo + +Default database changed to "foo" + +foo> /* llama */ +foo> use foo; + +Default database changed to "foo" + +foo> /* llama */ +foo> use foo + +Default database changed to "foo" + +foo> /* llama + /*> */use foo; + +Default database changed to "foo" + +foo> /* llama + /*> */use foo + +Default database changed to "foo" + +foo> /* llama + /*> */ use foo; + +Default database changed to "foo" + +foo> /* llama + /*> */ use foo + +Default database changed to "foo" + +foo> /* llama + /*> -- llama + /*> */use foo; + +Default database changed to "foo" + +foo> /* llama + /*> -- llama + /*> */use foo + +Default database changed to "foo" + +foo> /* llama + /*> -- llama + /*> */ use foo; + +Default database changed to "foo" + +foo> /* llama + /*> -- llama + /*> */ use foo + +Default database changed to "foo" + +foo> /* llama + /*> -- */use foo; + +Default database changed to "foo" + +foo> /* llama + /*> -- */use foo + +Default database changed to "foo" + +foo> /* llama + /*> -- */ use foo; + +Default database changed to "foo" + +foo> /* llama + /*> -- */ use foo + +Default database changed to "foo" + +foo> /* llama */ +foo> +foo> use foo;use bar; use foo + +Default database changed to "foo" + + +Default database changed to "bar" + + +Default database changed to "foo" + +foo> -- this isn't a command, but that should just print an error message and he +lp info. +foo> \xyz select 1 + +Unknown command. +delimiter \d Set statement delimiter. With no argument the delimiter resets to the default. +help \h Show the list of supported commands. +quit \q Terminates the session. +status \s Get status information. +teleport \t Show Teleport interactive shell information, such as execution limitations. +use \u Use another database. + +foo> \xyz;select 1; + +Unknown command. +delimiter \d Set statement delimiter. With no argument the delimiter resets to the default. +help \h Show the list of supported commands. +quit \q Terminates the session. +status \s Get status information. +teleport \t Show Teleport interactive shell information, such as execution limitations. +use \u Use another database. + + +1 +- +1 + +1 row in set + +foo> delimiter $$ +foo> help$$ + +delimiter \d Set statement delimiter. With no argument the delimiter resets to the default. +help \h Show the list of supported commands. +quit \q Terminates the session. +status \s Get status information. +teleport \t Show Teleport interactive shell information, such as execution limitations. +use \u Use another database. + +foo> \d // +foo> \h + +delimiter \d Set statement delimiter. With no argument the delimiter resets to the default. +help \h Show the list of supported commands. +quit \q Terminates the session. +status \s Get status information. +teleport \t Show Teleport interactive shell information, such as execution limitations. +use \u Use another database. + +foo> help + +delimiter \d Set statement delimiter. With no argument the delimiter resets to the default. +help \h Show the list of supported commands. +quit \q Terminates the session. +status \s Get status information. +teleport \t Show Teleport interactive shell information, such as execution limitations. +use \u Use another database. + +foo> status + +Teleport database: mysql-test-container +Current database: foo +Current user: root +Server version: 8.4.6 +Using delimiter: // + +foo> \s + +Teleport database: mysql-test-container +Current database: foo +Current user: root +Server version: 8.4.6 +Using delimiter: // + +foo> teleport + +Teleport MySQL interactive shell (v19.0.0-dev) + +Limitations: +- Query cancellation is not supported. Once a query is sent, its execution + can only be canceled by killing it from another session. + You can identify the query process ID with "SHOW PROCESSLIST" and then kill the + query with "KILL QUERY ". + +- Pressing CTRL-C will have no effect in this shell. + +foo> \t + +Teleport MySQL interactive shell (v19.0.0-dev) + +Limitations: +- Query cancellation is not supported. Once a query is sent, its execution + can only be canceled by killing it from another session. + You can identify the query process ID with "SHOW PROCESSLIST" and then kill the + query with "KILL QUERY ". + +- Pressing CTRL-C will have no effect in this shell. + +foo> use foo + +Default database changed to "foo" + +foo> \u mysql// + +Default database changed to "mysql" + +mysql> -- reset the delimiter to default ; +mysql> \d +mysql> teleport;\s;\h + +Teleport MySQL interactive shell (v19.0.0-dev) + +Limitations: +- Query cancellation is not supported. Once a query is sent, its execution + can only be canceled by killing it from another session. + You can identify the query process ID with "SHOW PROCESSLIST" and then kill the + query with "KILL QUERY ". + +- Pressing CTRL-C will have no effect in this shell. + + +Teleport database: mysql-test-container +Current database: mysql +Current user: root +Server version: 8.4.6 +Using delimiter: ; + + +delimiter \d Set statement delimiter. With no argument the delimiter resets to the default. +help \h Show the list of supported commands. +quit \q Terminates the session. +status \s Get status information. +teleport \t Show Teleport interactive shell information, such as execution limitations. +use \u Use another database. + +mysql> use `bar`;drop database foo;drop database bar;drop database test_commands +;quit + +Default database changed to "bar" + + +0 warnings, 0 rows affected + + +0 warnings, 0 rows affected + + +0 warnings, 0 rows affected + diff --git a/lib/client/db/mysql/repl/testdata/TestREPL/ddl/ddl.golden b/lib/client/db/mysql/repl/testdata/TestREPL/ddl/ddl.golden new file mode 100644 index 00000000000..f15877eec43 --- /dev/null +++ b/lib/client/db/mysql/repl/testdata/TestREPL/ddl/ddl.golden @@ -0,0 +1,221 @@ +Teleport MySQL interactive shell (v19.0.0-dev) +Connected to instance "mysql-test-container" as user "root". +Type 'help' or '\h' for help. + +(none)> -- temporarily disable note warnings for test consistency +(none)> set session sql_notes = 0; + +0 warnings, 0 rows affected + +(none)> DROP DATABASE IF EXISTS test_ddl_db; + +0 warnings, 0 rows affected + +(none)> CREATE DATABASE test_ddl_db; + +0 warnings, 1 rows affected + +(none)> set session sql_notes = 1; + +0 warnings, 0 rows affected + +(none)> -- this should produce a note level warning because it does exist +(none)> CREATE DATABASE if not exists test_ddl_db; + +1 warnings, 1 rows affected + +(none)> -- we clear the warning by selecting the warning count +(none)> select @@warning_count; + +@@warning_count +--------------- +1 + +1 row in set + +(none)> USE test_ddl_db; + +Default database changed to "test_ddl_db" + +test_ddl_db> +test_ddl_db> CREATE TABLE test_table ( + -> id INT PRIMARY KEY AUTO_INCREMENT, + -> value VARCHAR(50) NOT NULL, + -> note VARCHAR(100) DEFAULT NULL + -> ) ENGINE=InnoDB; + +0 warnings, 0 rows affected + +test_ddl_db> +test_ddl_db> CREATE VIEW test_view AS ( + -> SELECT id, value + -> FROM test_table + -> ); + +0 warnings, 0 rows affected + +test_ddl_db> +test_ddl_db> DELIMITER $$ +test_ddl_db> CREATE FUNCTION test_func(x INT) RETURNS INT DETERMINISTIC + -> BEGIN + -> RETURN x * 2; + -> END $$ + +0 warnings, 0 rows affected + +test_ddl_db> DELIMITER ; +test_ddl_db> +test_ddl_db> DELIMITER $$ +test_ddl_db> CREATE PROCEDURE test_proc() + -> BEGIN + -> INSERT INTO test_table(value) VALUES ('from proc'); + -> END $$ + +0 warnings, 0 rows affected + +test_ddl_db> DELIMITER ; +test_ddl_db> +test_ddl_db> CALL test_proc(); + +0 warnings, 1 rows affected + +test_ddl_db> +test_ddl_db> DELIMITER $$ +test_ddl_db> CREATE TRIGGER test_trigger_before_insert + -> BEFORE INSERT ON test_table + -> FOR EACH ROW + -> BEGIN + -> SET NEW.value = UPPER(NEW.value); + -> END $$ + +0 warnings, 0 rows affected + +test_ddl_db> DELIMITER ; +test_ddl_db> +test_ddl_db> CALL test_proc(); + +0 warnings, 1 rows affected + +test_ddl_db> +test_ddl_db> CREATE EVENT test_event + -> ON SCHEDULE AT CURRENT_TIMESTAMP + INTERVAL 1 HOUR + -> DO INSERT INTO test_table(value) VALUES ('from event'); + +0 warnings, 0 rows affected + +test_ddl_db> +test_ddl_db> set session sql_notes = 0; + +0 warnings, 0 rows affected + +test_ddl_db> -- roles are not database scoped, let's disable note warnings in ca +se some other +test_ddl_db> -- test has already created the role or user. +test_ddl_db> CREATE ROLE IF NOT EXISTS test_role; + +0 warnings, 0 rows affected + +test_ddl_db> DROP ROLE test_role; + +0 warnings, 0 rows affected + +test_ddl_db> +test_ddl_db> CREATE USER IF NOT EXISTS test_user@'localhost' + -> IDENTIFIED BY 'some password' + -> ACCOUNT LOCK; + +0 warnings, 0 rows affected + +test_ddl_db> ALTER USER test_user@'localhost' PASSWORD EXPIRE; + +0 warnings, 0 rows affected + +test_ddl_db> DROP USER test_user@'localhost'; + +0 warnings, 0 rows affected + +test_ddl_db> set session sql_notes = 1; + +0 warnings, 0 rows affected + +test_ddl_db> +test_ddl_db> ALTER DATABASE test_ddl_db CHARACTER SET utf8mb4; + +0 warnings, 1 rows affected + +test_ddl_db> ALTER TABLE test_table ADD COLUMN note2 VARCHAR(100) DEFAULT NULL; + +0 warnings, 0 rows affected + +test_ddl_db> ALTER EVENT test_event DISABLE; + +0 warnings, 0 rows affected + +test_ddl_db> ALTER FUNCTION test_func COMMENT 'Test function'; + +0 warnings, 0 rows affected + +test_ddl_db> ALTER PROCEDURE test_proc COMMENT 'Test procedure'; + +0 warnings, 0 rows affected + +test_ddl_db> +test_ddl_db> RENAME TABLE test_table + -> TO test_table_renamed; + +0 warnings, 0 rows affected + +test_ddl_db> SELECT * from test_table_renamed; + +id value note note2 +-- ----------- ---- ----- +1 'from proc' NULL NULL +2 'FROM PROC' NULL NULL + +2 rows in set + +test_ddl_db> +test_ddl_db> RENAME + -> TABLE test_table_renamed + -> TO test_table; + +0 warnings, 0 rows affected + +test_ddl_db> +test_ddl_db> SELECT * from test_table; + +id value note note2 +-- ----------- ---- ----- +1 'from proc' NULL NULL +2 'FROM PROC' NULL NULL + +2 rows in set + +test_ddl_db> +test_ddl_db> -- there is no "test_server", this should make an error. +test_ddl_db> ALTER SERVER test_server OPTIONS (HOST '127.0.0.1'); + +ERR ERROR 1477 (HY000): The foreign server name you are trying to reference does not exist. Data source error: test_server + +test_ddl_db> +test_ddl_db> DROP TRIGGER test_trigger_before_insert; + +0 warnings, 0 rows affected + +test_ddl_db> +test_ddl_db> TRUNCATE TABLE test_table ; + +0 warnings, 0 rows affected + +test_ddl_db> DROP TABLE test_table; + +0 warnings, 0 rows affected + +test_ddl_db> +test_ddl_db> DROP DATABASE test_ddl_db + -> ; + +0 warnings, 1 rows affected + +test_ddl_db> +test_ddl_db> \ No newline at end of file diff --git a/lib/client/db/mysql/repl/testdata/TestREPL/literals/literals.golden b/lib/client/db/mysql/repl/testdata/TestREPL/literals/literals.golden new file mode 100644 index 00000000000..0acea33bf3b --- /dev/null +++ b/lib/client/db/mysql/repl/testdata/TestREPL/literals/literals.golden @@ -0,0 +1,154 @@ +Teleport MySQL interactive shell (v19.0.0-dev) +Connected to instance "mysql-test-container" as user "root". +Type 'help' or '\h' for help. + +(none)> -- with ANSI_QUOTES double quotes are identifiers +(none)> SET SESSION sql_mode='ANSI_QUOTES'; + +0 warnings, 0 rows affected + +(none)> SELECT "thing"; + +ERR ERROR 1054 (42S22): Unknown column 'thing' in 'field list' + +(none)> -- without ANSI_QUOTES double quotes are literals +(none)> SET SESSION sql_mode=''; + +0 warnings, 0 rows affected + +(none)> SELECT "thing"; + +thing +------- +'thing' + +1 row in set + +(none)> select 1, -1,1.24, -1.24, 'foo', null, 'null'; + +1 -1 1.24 -1.24 foo NULL null +- -- ------ ------- ----- ---- ------ +1 -1 '1.24' '-1.24' 'foo' NULL 'null' + +1 row in set + +(none)> select + -> 1 as 'unsigned int', + -> -1 as 'negative int', + -> 1.24 as "float", + -> -1.24 as 'negative float', + -> 'foo' as 'string', + -> null as 'actual NULL', + -> "null" as 'string null'; + +unsigned int negative int float negative float string actual NULL string null +------------ ------------ ------ -------------- ------ ----------- ----------- +1 -1 '1.24' '-1.24' 'foo' NULL 'null' + +1 row in set + +(none)> +(none)> select 1 as 'multiple rows of literals' + -> union + -> select -1 + -> union + -> select 1.24 + -> union + -> select -1.24 + -> union + -> select 'foo' + -> union + -> select null + -> union + -> select 'null'; + +multiple rows of literals +------------------------- +'1' +'-1' +'1.24' +'-1.24' +'foo' +NULL +'null' + +7 rows in set + +(none)> +(none)> select 'a + '> b + '> c' as 'multiline-string'; + +multiline-string +---------------- +'a +b +c' + +1 row in set + +(none)> +(none)> select 'a + '> ; + '> b + '> ; + '> c' as 'multiline with delimiters'; + +multiline with delimiters +------------------------- +'a + ; + b + ; +c' + +1 row in set + +(none)> +(none)> /* foo bar */ +(none)> select 1; + +1 +- +1 + +1 row in set + +(none)> /* foo bar */select /* + /*> baz */ 42; + +42 +-- +42 + +1 row in set + +(none)> +(none)> delimiter $$ +(none)> select '$$1$$'$$ + +$$1$$ +------- +'$$1$$' + +1 row in set + +(none)> select '$$;$$';$$ + +$$;$$ +------- +'$$;$$' + +1 row in set + +(none)> delimiter ; +(none)> select 1; + +1 +- +1 + +1 row in set + +(none)> +(none)> \ No newline at end of file diff --git a/lib/client/db/mysql/repl/testdata/Test_formatResult/empty_set/empty-set.golden b/lib/client/db/mysql/repl/testdata/Test_formatResult/empty_set/empty-set.golden new file mode 100644 index 00000000000..4883fe4c84b --- /dev/null +++ b/lib/client/db/mysql/repl/testdata/Test_formatResult/empty_set/empty-set.golden @@ -0,0 +1 @@ +Empty set (0.04 sec) \ No newline at end of file diff --git a/lib/client/db/mysql/repl/testdata/Test_formatResult/long_elapsed_time/long-elapsed-time.golden b/lib/client/db/mysql/repl/testdata/Test_formatResult/long_elapsed_time/long-elapsed-time.golden new file mode 100644 index 00000000000..6f8d49064fe --- /dev/null +++ b/lib/client/db/mysql/repl/testdata/Test_formatResult/long_elapsed_time/long-elapsed-time.golden @@ -0,0 +1,7 @@ +one two three +--- --- ----- +1 2.2 '3' +4 5.5 '6' +7 8.8 '8' + +3 rows in set (123.46 sec) \ No newline at end of file diff --git a/lib/client/db/mysql/repl/testdata/Test_formatResult/nil_set/nil-set.golden b/lib/client/db/mysql/repl/testdata/Test_formatResult/nil_set/nil-set.golden new file mode 100644 index 00000000000..3861e046317 --- /dev/null +++ b/lib/client/db/mysql/repl/testdata/Test_formatResult/nil_set/nil-set.golden @@ -0,0 +1 @@ +2 warnings, 42 rows affected (0.00 sec) \ No newline at end of file diff --git a/lib/client/db/mysql/repl/testdata/Test_formatResult/result_rows/result-rows.golden b/lib/client/db/mysql/repl/testdata/Test_formatResult/result_rows/result-rows.golden new file mode 100644 index 00000000000..efb91d451b8 --- /dev/null +++ b/lib/client/db/mysql/repl/testdata/Test_formatResult/result_rows/result-rows.golden @@ -0,0 +1,7 @@ +one two three +--- --- ----- +1 2.2 '3' +4 5.5 '6' +7 8.8 '8' + +3 rows in set (1.23 sec) \ No newline at end of file diff --git a/lib/client/db/mysql/repl/token.go b/lib/client/db/mysql/repl/token.go new file mode 100644 index 00000000000..46722ec9fd3 --- /dev/null +++ b/lib/client/db/mysql/repl/token.go @@ -0,0 +1,53 @@ +// Teleport +// Copyright (C) 2025 Gravitational, Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +package repl + +// token is a lexical token. +type token struct { + // kind is a token kind enum. + kind tokenKind + // text is the text that makes up the token. + text string +} + +type tokenKind int + +const ( + // EOF indicates end of token stream + tokenEOF tokenKind = iota + + // whitespace + tokenSpace + + // escape + tokenBackslash // \ + + // quotation syntax + tokenSingleQuote // ' + tokenDoubleQuote // " + tokenBacktick // ` + + // single-line comments + tokenSingleComment // -- or # + + // multi-line comment open/close + tokenOpenComment // /* + tokenCloseComment // */ + + // misc text that isn't special to us + tokenText +) diff --git a/lib/service/service.go b/lib/service/service.go index 14e0759476c..871dc35a536 100644 --- a/lib/service/service.go +++ b/lib/service/service.go @@ -111,6 +111,7 @@ import ( _ "github.com/gravitational/teleport/lib/backend/pgbk" "github.com/gravitational/teleport/lib/bpf" "github.com/gravitational/teleport/lib/cache" + myrepl "github.com/gravitational/teleport/lib/client/db/mysql/repl" pgrepl "github.com/gravitational/teleport/lib/client/db/postgres/repl" dbrepl "github.com/gravitational/teleport/lib/client/db/repl" "github.com/gravitational/teleport/lib/cloud" @@ -1163,6 +1164,7 @@ func NewTeleport(cfg *servicecfg.Config) (_ *TeleportProcess, err error) { cfg.DatabaseREPLRegistry = dbrepl.NewREPLGetter(map[string]dbrepl.REPLNewFunc{ defaults.ProtocolPostgres: pgrepl.New, defaults.ProtocolCockroachDB: pgrepl.New, + defaults.ProtocolMySQL: myrepl.New, }) }