From 96ed7b13693693c9bbd7fd77b686c550327187f8 Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 21 Jun 2023 11:03:20 +0100 Subject: [PATCH 01/12] Model status parser --- go.mod | 38 ++++- go.sum | 82 ++++++++- internal/jimm/model_status_parser.go | 239 +++++++++++++++++++++++++++ 3 files changed, 343 insertions(+), 16 deletions(-) create mode 100644 internal/jimm/model_status_parser.go diff --git a/go.mod b/go.mod index 525c1fd96..056032ecd 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/gorilla/websocket v1.5.0 github.com/gosuri/uitable v0.0.1 github.com/hashicorp/vault/api v1.8.2 + github.com/itchyny/gojq v0.12.13 github.com/jackc/pgconn v1.7.0 github.com/jackc/pgx/v4 v4.9.0 github.com/juju/charm/v10 v10.0.0 @@ -57,6 +58,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/network/armnetwork v1.0.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.0.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.0.0 // indirect + github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest v0.11.24 // indirect github.com/Azure/go-autorest/autorest/adal v0.9.18 // indirect @@ -67,6 +69,7 @@ require ( github.com/Azure/go-autorest/tracing v0.6.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v0.5.1 // indirect github.com/Rican7/retry v0.3.1 // indirect + github.com/adrg/xdg v0.3.3 // indirect github.com/armon/go-metrics v0.3.10 // indirect github.com/armon/go-radix v1.0.0 // indirect github.com/aws/aws-sdk-go-v2 v1.9.1 // indirect @@ -91,6 +94,7 @@ require ( github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.3.2 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + github.com/creack/pty v1.1.15 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/distribution v2.7.1+incompatible // indirect github.com/duo-labs/webauthn v0.0.0-20220815211337-00c9fb5711f5 // indirect @@ -100,8 +104,11 @@ require ( github.com/fatih/color v1.13.0 // indirect github.com/flosch/pongo2 v0.0.0-20200913210552-0d938eb266f3 // indirect github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect + github.com/fsnotify/fsnotify v1.5.4 // indirect github.com/fullstorydev/grpcurl v1.8.1 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect + github.com/gdamore/encoding v1.0.0 // indirect + github.com/gdamore/tcell/v2 v2.5.1 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-logr/logr v1.2.2 // indirect github.com/go-macaroon-bakery/macaroonpb v1.0.0 // indirect @@ -148,6 +155,7 @@ require ( github.com/im7mortal/kmutex v1.0.1 // indirect github.com/imdario/mergo v0.3.12 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/jackc/chunkreader/v2 v2.0.1 // indirect github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect @@ -197,6 +205,7 @@ require ( github.com/juju/usso v1.0.1 // indirect github.com/juju/utils v0.0.0-20200604140309-9d78121a29e0 // indirect github.com/juju/utils/v3 v3.0.2 // indirect + github.com/juju/viddy v0.0.0-beta5 // indirect github.com/juju/webbrowser v1.0.0 // indirect github.com/juju/worker/v3 v3.1.0 // indirect github.com/julienschmidt/httprouter v1.3.0 // indirect @@ -211,11 +220,13 @@ require ( github.com/lestrrat/go-jsval v0.0.0-20161012045717-b1258a10419f // indirect github.com/lestrrat/go-pdebug v0.0.0-20160817063333-2e6eaaa5717f // indirect github.com/lestrrat/go-structinfo v0.0.0-20160308131105-f74c056fe41f // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lunixbochs/vtclean v1.0.0 // indirect github.com/lxc/lxd v0.0.0-20220816180258-7e0418163fa9 // indirect + github.com/magiconair/properties v1.8.6 // indirect github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect github.com/mitchellh/copystructure v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect @@ -225,6 +236,7 @@ require ( github.com/mitchellh/reflectwalk v1.0.1 // indirect github.com/mittwald/vaultgo v0.1.1 // indirect github.com/moby/spdystream v0.2.0 // indirect + github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect @@ -234,6 +246,8 @@ require ( github.com/onsi/gomega v1.10.4 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/pborman/uuid v1.2.1 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pelletier/go-toml/v2 v2.0.2 // indirect github.com/pierrec/lz4 v2.5.2+incompatible // indirect github.com/pkg/browser v0.0.0-20210115035449-ce105d075bb4 // indirect github.com/pkg/errors v0.9.1 // indirect @@ -241,16 +255,23 @@ require ( github.com/prometheus/client_model v0.2.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect - github.com/rivo/uniseg v0.2.0 // indirect + github.com/rivo/tview v0.0.0-20220610163003-691f46d6f500 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/rs/xid v1.4.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/sergi/go-diff v1.2.0 // indirect github.com/sirupsen/logrus v1.9.0 // indirect github.com/soheilhy/cmux v0.1.5 // indirect + github.com/spf13/afero v1.9.2 // indirect + github.com/spf13/cast v1.5.0 // indirect github.com/spf13/cobra v1.5.0 // indirect + github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.12.0 // indirect + github.com/subosito/gotenv v1.4.0 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect github.com/urfave/cli v1.22.5 // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect @@ -260,10 +281,10 @@ require ( github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect github.com/yohcop/openid-go v1.0.0 // indirect go.etcd.io/bbolt v1.3.5 // indirect - go.etcd.io/etcd/api/v3 v3.5.0 // indirect - go.etcd.io/etcd/client/pkg/v3 v3.5.0 // indirect - go.etcd.io/etcd/client/v2 v2.305.0 // indirect - go.etcd.io/etcd/client/v3 v3.5.0-alpha.0 // indirect + go.etcd.io/etcd/api/v3 v3.5.4 // indirect + go.etcd.io/etcd/client/pkg/v3 v3.5.4 // indirect + go.etcd.io/etcd/client/v2 v2.305.4 // indirect + go.etcd.io/etcd/client/v3 v3.5.4 // indirect go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0 // indirect go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 // indirect go.etcd.io/etcd/raft/v3 v3.5.0-alpha.0 // indirect @@ -276,7 +297,7 @@ require ( golang.org/x/mod v0.9.0 // indirect golang.org/x/net v0.8.0 // indirect golang.org/x/oauth2 v0.6.0 // indirect - golang.org/x/sys v0.6.0 // indirect + golang.org/x/sys v0.8.0 // indirect golang.org/x/term v0.6.0 // indirect golang.org/x/text v0.9.0 // indirect golang.org/x/time v0.3.0 // indirect @@ -290,6 +311,7 @@ require ( gopkg.in/gobwas/glob.v0 v0.2.3 // indirect gopkg.in/httprequest.v1 v1.2.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect + gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/juju/environschema.v1 v1.0.1 // indirect gopkg.in/macaroon-bakery.v3 v3.0.0 // indirect gopkg.in/mgo.v2 v2.0.0-20190816093944-a6b53ec6cb22 // indirect diff --git a/go.sum b/go.sum index 91ea6c8e3..d22de9a49 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,7 @@ cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOY cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= @@ -45,6 +46,7 @@ cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0Zeo cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= code.gitea.io/sdk/gitea v0.11.3/go.mod h1:z3uwDV/b9Ls47NGukYM9XhnHtqPh/J+t40lsUrR6JDY= contrib.go.opencensus.io/exporter/aws v0.0.0-20181029163544-2befc13012d0/go.mod h1:uu1P0UCM/6RbsMrgPa98ll8ZcHM858i/AD06a9aLRCA= contrib.go.opencensus.io/exporter/ocagent v0.5.0/go.mod h1:ImxhfLRpxoYiSq891pBrLVhN+qmP8BTVvdH2YLs7Gl0= @@ -86,6 +88,7 @@ github.com/Azure/azure-service-bus-go v0.9.1/go.mod h1:yzBx6/BUGfjfeqbRZny9AQIbI github.com/Azure/azure-storage-blob-go v0.8.0/go.mod h1:lPI3aLPpuLTeUwh1sViKXFxwl2B6teiRqI0deQUvsw0= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= +github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-autorest v10.8.1+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v12.0.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= @@ -163,6 +166,8 @@ github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:H github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/adrg/xdg v0.3.3 h1:s/tV7MdqQnzB1nKY8aqHvAMD+uCiuEDzVB5HLRY849U= +github.com/adrg/xdg v0.3.3/go.mod h1:61xAR2VZcggl2St4O9ohF5qCKe08+JDmE4VNzPFQvOQ= github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alcortesm/tgz v0.0.0-20161220082320-9c5fe88206d7/go.mod h1:6zEj6s6u/ghQa61ZWa/C2Aw3RkjiTBOix7dkqa1VLIs= @@ -409,6 +414,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.11/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/creack/pty v1.1.15 h1:cKRCLMj3Ddm54bKSpemfQ8AtYFBhAI2MPmdys22fBdc= +github.com/creack/pty v1.1.15/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyphar/filepath-securejoin v0.2.2/go.mod h1:FpkQEhXnPnOthhzymB7CGsFk2G9VLXONKD9G7QGMM+4= github.com/d2g/dhcp4 v0.0.0-20170904100407-a1d1b6c41b1c/go.mod h1:Ct2BUK8SB0YC1SMSibvLzxjeJLnrYEVLULFNiHY9YfQ= github.com/d2g/dhcp4client v1.0.0/go.mod h1:j0hNfjhrt2SxUOw55nL0ATM/z4Yt3t2Kd1mW34z5W5s= @@ -507,6 +514,7 @@ github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUork github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= +github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/fullsailor/pkcs7 v0.0.0-20190404230743-d7302db945fa/go.mod h1:KnogPXtdwXqoenmZCw6S+25EAm2MkxbG0deNDu4cbSA= github.com/fullstorydev/grpcurl v1.8.0/go.mod h1:Mn2jWbdMrQGJQ8UD62uNyMumT2acsZUCkZIqFxsQf1o= github.com/fullstorydev/grpcurl v1.8.1 h1:Pp648wlTTg3OKySeqxM5pzh8XF6vLqrm8wRq66+5Xo0= @@ -514,6 +522,11 @@ github.com/fullstorydev/grpcurl v1.8.1/go.mod h1:3BWhvHZwNO7iLXaQlojdg5NA6SxUDeP github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88= github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo= github.com/garyburd/redigo v0.0.0-20150301180006-535138d7bcd7/go.mod h1:NR3MbYisc3/PwhQ00EMzDiPmrwpPxAn5GI05/YaO1SY= +github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko= +github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= +github.com/gdamore/tcell/v2 v2.4.1-0.20210905002822-f057f0a857a1/go.mod h1:Az6Jt+M5idSED2YPGtwnfJV0kXohgdCBPmHGSYc1r04= +github.com/gdamore/tcell/v2 v2.5.1 h1:zc3LPdpK184lBW7syF2a5C6MV827KmErk9jGVnmsl/I= +github.com/gdamore/tcell/v2 v2.5.1/go.mod h1:wSkrPaXoiIWZqW/g7Px4xc79di6FTcpB8tvaKJ6uGBo= github.com/getkin/kin-openapi v0.76.0/go.mod h1:660oXbgy5JFMKreazJaQTw7o+X00qeSyhcnluiMv+Xg= github.com/getsentry/raven-go v0.2.0 h1:no+xWJRb5ZI7eE8TWgIq1jLulQiIoLG0IfYxv5JYMGs= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= @@ -682,6 +695,7 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= @@ -709,6 +723,7 @@ github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3i github.com/googleapis/gnostic v0.5.1/go.mod h1:6U4PtQXGIEt/Z3h5MAT7FNofLnw9vXk2cUuW7uA/OeU= github.com/googleapis/gnostic v0.5.5 h1:9fHAtK0uDfpveeqqo1hkEZJcFvYXAiCN3UutL8F9xHw= github.com/googleapis/gnostic v0.5.5/go.mod h1:7+EbHbldMins07ALC74bsA81Ovc97DwqyJO1AENw9kA= +github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gordonklaus/ineffassign v0.0.0-20200309095847-7953dde2c7bf/go.mod h1:cuNKsD1zp2v6XfE/orVX2QE1LC+i254ceGcVeDT3pTU= github.com/goreleaser/goreleaser v0.134.0/go.mod h1:ZT6Y2rSYa6NxQzIsdfWWNWAlYGXGbreo66NmE+3X3WQ= @@ -855,6 +870,10 @@ github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= +github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4= +github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= +github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= github.com/j-keck/arping v0.0.0-20160618110441-2cf9dc699c56/go.mod h1:ymszkNOg6tORTn+6F6j+Jc8TOr5osrynvN6ivFWZ2GA= github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= @@ -1118,6 +1137,8 @@ github.com/juju/version/v2 v2.0.0-20211007103408-2e8da085dc23/go.mod h1:Ljlbryh9 github.com/juju/version/v2 v2.0.0/go.mod h1:ZeFjNy+UFEWJDDPdzW7Cm9NeU6dsViGaFYhXzycLQrw= github.com/juju/version/v2 v2.0.1 h1:4psP9XDv8qIbSdv3llCmcmBXe+wvm57BvYp2sG/i6zc= github.com/juju/version/v2 v2.0.1/go.mod h1:OK+DKO9ve3fFCVUZGT8ZbUWU2TnZh914S6gMhiM2hXg= +github.com/juju/viddy v0.0.0-beta5 h1:aMSljAcSEWoWC+KGDOd2cEUyIB/EzSDbWxa5lchTWfU= +github.com/juju/viddy v0.0.0-beta5/go.mod h1:5Yvv+opdIEDt15Tt0++8YvccVykcGJeVXxqVnGqUXVE= github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4/go.mod h1:G6PCelgkM6cuvyD10iYJsjLBsSadVXtJ+nBxFAxE2BU= github.com/juju/webbrowser v1.0.0 h1:JLdmbFtCGY6Qf2jmS6bVaenJFGIFkdF1/BjUm76af78= github.com/juju/webbrowser v1.0.0/go.mod h1:RwVlbBcF91Q4vS+iwlkJ6bZTE3EwlrjbYlM3WMVD6Bc= @@ -1185,6 +1206,8 @@ github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lunixbochs/vtclean v0.0.0-20160125035106-4fbf7632a2c6/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= github.com/lunixbochs/vtclean v1.0.0 h1:xu2sLAri4lGiovBDQKxl5mrXyESr3gUr5m5SM5+LVb8= github.com/lunixbochs/vtclean v1.0.0/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI= @@ -1195,6 +1218,8 @@ github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0Q github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= +github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= @@ -1228,15 +1253,17 @@ github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcME github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= github.com/mattn/go-shellwords v1.0.10/go.mod h1:EZzvwXDESEeg03EKmM+RmDnNOPKG4lLtQsUlTZDWQ8Y= github.com/mattn/go-sqlite3 v1.14.5 h1:1IdxlwTNazvbKJQSxoJ5/9ECbEeaTTyeU7sEAZ5KKTQ= @@ -1287,6 +1314,7 @@ github.com/moby/sys/symlink v0.1.0/go.mod h1:GGDODQmbFOjFsXvfLVn3+ZRxkch54RkSiGq github.com/moby/term v0.0.0-20200312100748-672ec06f55cd/go.mod h1:DdlQx2hp0Ss5/fLikoLlEeIYiATotOjgB//nb973jeo= github.com/moby/term v0.0.0-20201216013528-df9cb8a40635/go.mod h1:FBS0z0QWA44HXygs7VXDUOGoN/1TV3RuWkLO04am3wc= github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 h1:dcztxKSvZ4Id8iPpHERQBbIJfabdt4wUm5qy3wOL2Zc= +github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6/go.mod h1:E2VnQOmVuvZB6UYnnDB0qG5Nq/1tD9acaOpo6xmt0Kw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -1398,6 +1426,10 @@ github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtP github.com/pelletier/go-buffruneio v0.2.0/go.mod h1:JkE26KsDizTr40EUHkXVtNPvgGtbSNq5BcowyYOWdKo= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= +github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0= @@ -1415,6 +1447,7 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= +github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pkg/sftp v1.13.5 h1:a3RLUqkyjYRtBTZJZ1VRrKbN3zhuPLlUc3sphVz81go= github.com/pkg/sftp v1.13.5/go.mod h1:wHDZ0IZX6JcBYRK1TH9bcVq8G7TLpVHYIGJRFnmPfxg= github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -1436,6 +1469,7 @@ github.com/prometheus/client_golang v1.5.1/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3O github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.10.0/go.mod h1:WJM3cc3yu7XKBKa/I8WeZm+V3eltZnBwfENSU7mdogU= github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_golang v1.12.1/go.mod h1:3Z9XVyYiZYEO+YQWt3RD2R3jrbd179Rt297l4aS6nDY= github.com/prometheus/client_golang v1.13.0 h1:b71QUfeo5M8gq2+evJdTPfZhYMAU0uKPkyPJ7TPsloU= github.com/prometheus/client_golang v1.13.0/go.mod h1:vTeo+zgvILHsnnj/39Ou/1fPN5nJFOEMgftOUOmlvYQ= @@ -1484,9 +1518,12 @@ github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40T github.com/pseudomuto/protoc-gen-doc v1.4.1/go.mod h1:exDTOVwqpp30eV/EDPFLZy3Pwr2sn6hBC1WIYH/UbIg= github.com/pseudomuto/protokit v0.2.0/go.mod h1:2PdH30hxVHsup8KpBTOXTBeMVhJZVio3Q8ViKSAXT0Q= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rivo/tview v0.0.0-20220610163003-691f46d6f500 h1:KvoRB2TMfMqK2NF2mIvZprDT/Ofvsa4RphWLoCmUDag= +github.com/rivo/tview v0.0.0-20220610163003-691f46d6f500/go.mod h1:WIfMkQNY+oq/mWwtsjOYHIZBuwthioY2srOmljJkTnk= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9Se4YQIRkDTCw1EJls9xTUCaCeRM= @@ -1521,6 +1558,7 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= +github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY= @@ -1554,8 +1592,12 @@ github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTd github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= +github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= +github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= +github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= +github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU= @@ -1566,6 +1608,7 @@ github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t6 github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= +github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1-0.20171106142849-4c012f6dcd95/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -1577,6 +1620,8 @@ github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DM github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg= github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns= +github.com/spf13/viper v1.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= +github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= github.com/src-d/gcfg v1.4.0/go.mod h1:p/UMsR43ujA89BJY9duynAwIpvqEujIH/jFlfL7jWoI= github.com/stefanberger/go-pkcs11uri v0.0.0-20201008174630-78d3cae3a980/go.mod h1:AO3tvPzVZ/ayst6UlUKUv6rcPQInYe3IknH3jYhAKu8= github.com/stoewer/go-strcase v1.2.0/go.mod h1:IBiWB2sKIp3wVVQ3Y035++gc+knqhUQag1KpM8ahLw8= @@ -1596,8 +1641,11 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= +github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= github.com/syndtr/gocapability v0.0.0-20170704070218-db04d3cc01c8/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20180916011248-d98352740cb2/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= @@ -1686,15 +1734,19 @@ go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd v0.5.0-alpha.5.0.20200910180754-dd1b699fc489/go.mod h1:yVHk9ub3CSBatqGNg7GRmsnfLWtoW60w4eDYfh7vHDg= go.etcd.io/etcd/api/v3 v3.5.0-alpha.0/go.mod h1:mPcW6aZJukV6Aa81LSKpBjQXTWlXB5r74ymPoSWa3Sw= -go.etcd.io/etcd/api/v3 v3.5.0 h1:GsV3S+OfZEOCNXdtNkBSR7kgLobAa/SO6tCxRa0GAYw= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= -go.etcd.io/etcd/client/pkg/v3 v3.5.0 h1:2aQv6F436YnN7I4VbI8PPYrBhu+SmrTaADcf8Mi/6PU= +go.etcd.io/etcd/api/v3 v3.5.4 h1:OHVyt3TopwtUQ2GKdd5wu3PmmipR4FTwCqoEjSyRdIc= +go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= +go.etcd.io/etcd/client/pkg/v3 v3.5.4 h1:lrneYvz923dvC14R54XcA7FXoZ3mlGZAgmwhfm7HqOg= +go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0-alpha.0/go.mod h1:kdV+xzCJ3luEBSIeQyB/OEKkWKd8Zkux4sbDeANrosU= -go.etcd.io/etcd/client/v2 v2.305.0 h1:ftQ0nOOHMcbMS3KIaDQ0g5Qcd6bhaBrQT6b89DfwLTs= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= -go.etcd.io/etcd/client/v3 v3.5.0-alpha.0 h1:dr1EOILak2pu4Nf5XbRIOCNIBjcz6UmkQd7hHRXwxaM= +go.etcd.io/etcd/client/v2 v2.305.4 h1:Dcx3/MYyfKcPNLpR4VVQUP5KgYrBeJtktBwEKkw08Ao= +go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= go.etcd.io/etcd/client/v3 v3.5.0-alpha.0/go.mod h1:wKt7jgDgf/OfKiYmCq5WFGxOFAkVMLxiiXgLDFhECr8= +go.etcd.io/etcd/client/v3 v3.5.4 h1:p83BUL3tAYS0OT/r0qglgc3M1JjhM0diV8DSWAhVXv4= +go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0 h1:odMFuQQCg0UmPd7Cyw6TViRYv9ybGuXuki4CusDSzqA= go.etcd.io/etcd/etcdctl/v3 v3.5.0-alpha.0/go.mod h1:YPwSaBciV5G6Gpt435AasAG3ROetZsKNUzibRa/++oo= go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0 h1:3yLUEC0nFCxw/RArImOyRUI4OAFbg4PFpBbAhSNzKNY= @@ -1774,10 +1826,12 @@ golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201112155050-0c6587e931a9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= +golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210506145944-38f3c27a63bf/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= @@ -2017,6 +2071,7 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210309074719-68d13333faf2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -2026,6 +2081,7 @@ golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -2038,13 +2094,17 @@ golang.org/x/sys v0.0.0-20210831042530-f4d43177bf5e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -2141,6 +2201,7 @@ golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= @@ -2242,7 +2303,9 @@ google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -2339,6 +2402,7 @@ gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= +gopkg.in/ini.v1 v1.66.6/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/juju/environschema.v1 v1.0.0/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA= gopkg.in/juju/environschema.v1 v1.0.1 h1:eXQQsfSJykpp1Kz79pVmKWE6G5yzKuiCqkR01LHFVS0= gopkg.in/juju/environschema.v1 v1.0.1/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA= @@ -2384,8 +2448,10 @@ gorm.io/gorm v1.20.4/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.5/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= gorm.io/gorm v1.20.6 h1:qa7tC1WcU+DBI/ZKMxvXy1FcrlGsvxlaKufHrT2qQ08= gorm.io/gorm v1.20.6/go.mod h1:0HFTzE/SqkGTzK6TlDPPQbAYCluiVvhzoA1+aVyzenw= +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.0.2/go.mod h1:3SzNCllyD9/Y+b5r9JIKQ474KzkZyqLqEfYqMsX94Bk= +gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0= gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8= honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/jimm/model_status_parser.go b/internal/jimm/model_status_parser.go new file mode 100644 index 000000000..6b7852f77 --- /dev/null +++ b/internal/jimm/model_status_parser.go @@ -0,0 +1,239 @@ +package jimm + +import ( + "context" + "database/sql" + "encoding/json" + "strings" + + "github.com/CanonicalLtd/jimm/api/params" + "github.com/CanonicalLtd/jimm/internal/dbmodel" + "github.com/CanonicalLtd/jimm/internal/errors" + "github.com/itchyny/gojq" + "go.uber.org/zap" + + jujucmd "github.com/juju/cmd/v3" + "github.com/juju/juju/cmd/juju/status" + "github.com/juju/juju/cmd/juju/storage" + rpcparams "github.com/juju/juju/rpc/params" + "github.com/juju/zaputil/zapctx" +) + +// QueryModels queries every model available to a given user. +// +// The jqQuery must be a valid jq query and can return every result, even iterative listings. +// If a result is erroneous, for example, bad data type parsing, the resulting struct field +// Errors will contain a map from model UUID -> []error. Otherwise, the Results field +// will contain model UUID -> []Jq result. +func (j *JIMM) QueryModelsJq(ctx context.Context, modelUUIDs []string, jqQuery string) (params.CrossModelQueryResponse, error) { + op := errors.Op("QueryModels") + results := params.CrossModelQueryResponse{ + Results: make(map[string][]any), + Errors: make(map[string][]string), + } + + query, err := gojq.Parse(jqQuery) + if err != nil { + return results, errors.E(op, err) + } + + // We remove "model:" from the UUIDs, unfortunately that's what OpenFGA returns now after + // recent versions. + for i := range modelUUIDs { + modelUUIDs[i] = strings.Split(modelUUIDs[i], ":")[1] + } + + // Set up a formatterParamsRetriever to handle the heavy lifting + // of each facade call and type conversion. + retriever := newFormatterParamsRetriever(j) + + for _, id := range modelUUIDs { + + params, err := retriever.GetParams(ctx, id) + if err != nil { + zapctx.Error(ctx, "failed to get status formatter params", zap.String("model-uuid", id)) + results.Errors[id] = append(results.Errors[id], err.Error()) + continue + } + + // We use very specific formatting parameters to ensure like-for-like output + // with the default juju client installation performing a "status --format json". + formatter := status.NewStatusFormatter(*params) + + formattedStatus, err := formatter.Format() + if err != nil { + zapctx.Error(ctx, "failed to format status", zap.String("model-uuid", id)) + results.Errors[id] = append(results.Errors[id], err.Error()) + continue + } + // We could use output.NewFormatter() from 3.0+ juju/juju, but ultimately + // we just want some JSON output, regardless of user formatting. As such json.Marshal + // *should* be OK. But TODO: make sure this is fine. + fb, err := json.Marshal(formattedStatus) + if err != nil { + zapctx.Error(ctx, "failed to marshal formatted status", zap.String("model-uuid", id)) + results.Errors[id] = append(results.Errors[id], err.Error()) + continue + } + tempMap := make(map[string]any) + if err := json.Unmarshal(fb, &tempMap); err != nil { + return results, errors.E(op, err) + } + queryIter := query.RunWithContext(ctx, tempMap) + + for { + v, ok := queryIter.Next() + if !ok { + break + } + + // Jq errors can range from one failure in an iterative query to an entirely broken + // query. As such, we simply append all to the errors field and continue to collect + // both erreoneous and valid query results. + if err, ok := v.(error); ok { + results.Errors[id] = append(results.Errors[id], "jq error: "+err.Error()) + continue + } + + results.Results[id] = append(results.Results[id], v) + } + } + return results, nil +} + +// formatterParamsRetriever is a self-contained block of +// parameter retrieval for Juju's status.NewStatusFormatter. +// +// It handles the retrieval of all parameters to properly format them into +// sensible outputs. +// +// First, call LoadModel, this will retrieve a model from JIMM's database. +// Next, simply call GetParams. +type formatterParamsRetriever struct { + model *dbmodel.Model + jimm *JIMM + api API +} + +// newFormatterParamsRetriever returns a formatterParamsRetriever. +func newFormatterParamsRetriever(j *JIMM) *formatterParamsRetriever { + return &formatterParamsRetriever{ + jimm: j, + } +} + +// GetParams retrieves the required parameters for the Juju status formatter from the currently +// loaded model. See formatterParamsRetriever.LoadModel for more information. +func (f *formatterParamsRetriever) GetParams(ctx context.Context, modelUUID string) (*status.NewStatusFormatterParams, error) { + if err := f.loadModel(ctx, modelUUID); err != nil { + return nil, err + } + + err := f.dialModel(ctx) + if err != nil { + return nil, err + } + defer f.api.Close() + + modelStatus, err := f.getModelStatus(ctx) + if err != nil { + return nil, err + } + + combinedStorage, err := f.getCombinedStorageInfo(ctx) + if err != nil { + return nil, err + } + + return &status.NewStatusFormatterParams{ + ControllerName: f.model.Controller.Name, + Status: modelStatus, + Storage: combinedStorage, + ShowRelations: true, + ISOTime: true, + }, nil +} + +// LoadModel loads the model by UUID from the database into the formatterParamsRetriever. +// This MUST be called before attempting to GetParams(). +func (f *formatterParamsRetriever) loadModel(ctx context.Context, modelUUID string) error { + model := dbmodel.Model{ + UUID: sql.NullString{String: modelUUID, Valid: true}, + } + + if err := f.jimm.Database.GetModel(ctx, &model); err != nil { + zapctx.Error(ctx, "failed to retrieve model", zap.String("model-uuid", modelUUID)) + return err + } + f.model = &model + return nil +} + +// dialModel dials the model currently loaded into the formatterParamsRetriever. +func (f *formatterParamsRetriever) dialModel(ctx context.Context) error { + api, err := f.jimm.dial(ctx, &f.model.Controller, f.model.ResourceTag()) + if err != nil { + zapctx.Error(ctx, "failed to dial controller for model", zap.String("controller-uuid", f.model.Controller.UUID), zap.String("model-uuid", f.model.UUID.String), zap.Error(err)) + } + f.api = api + return err +} + +// getModelStatus calls the FullStatus facade to return the full status for the current model +// loaded in the formatterParamsRetriever. +func (f *formatterParamsRetriever) getModelStatus(ctx context.Context) (*rpcparams.FullStatus, error) { + modelStatus, err := f.api.Status(ctx, nil) + if err != nil { + zapctx.Error(ctx, "failed to call FullStatus", zap.String("controller-uuid", f.model.Controller.UUID), zap.String("model-uuid", f.model.UUID.String), zap.Error(err)) + } + return modelStatus, err +} + +func (f *formatterParamsRetriever) getCombinedStorageInfo(ctx context.Context) (*storage.CombinedStorage, error) { + storageAPI := newStorageListAPI(ctx, f.api) + + // We use cmdCtx lightly, it's simply passed to the params but is only used for some + // logging. + cmdCtx, _ := jujucmd.DefaultContext() + + return storage.GetCombinedStorageInfo(storage.GetCombinedStorageInfoParams{ + Context: cmdCtx, + APIClient: &storageAPI, + Ids: []string{}, + WantStorage: true, + WantVolumes: true, + WantFilesystems: true, + }) +} + +// storageListAPI acts as a wrapper over our implementation of the juju client, seen in ./internal/jujuclient. +// This enables us to use storage.GetCombinedStorageInfo without having to c/p the logic we require. +type storageListAPI struct { + ctx context.Context + api API +} + +// newStorageListAPI returns a new storageListAPI. +func newStorageListAPI(ctx context.Context, api API) storageListAPI { + return storageListAPI{ctx, api} +} + +// ListStorageDetails implements storage.StorageListAPI. (From Juju) +func (s *storageListAPI) ListStorageDetails() ([]rpcparams.StorageDetails, error) { + return s.api.ListStorageDetails(s.ctx) +} + +// ListFilesystems implements storage.StorageListAPI. (From Juju) +func (s *storageListAPI) ListFilesystems(machines []string) ([]rpcparams.FilesystemDetailsListResult, error) { + return s.api.ListFilesystems(s.ctx, machines) +} + +// ListVolumes implements storage.StorageListAPI. (From Juju) +func (s *storageListAPI) ListVolumes(machines []string) ([]rpcparams.VolumeDetailsListResult, error) { + return s.api.ListVolumes(s.ctx, machines) +} + +// Close implements storage.StorageListAPI. (From Juju) +func (s *storageListAPI) Close() error { + return s.api.Close() +} From 9326fd026b3c103f63cee74f81348f75e995989d Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 21 Jun 2023 11:05:04 +0100 Subject: [PATCH 02/12] Bring in API definition --- internal/jimm/jimm.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/jimm/jimm.go b/internal/jimm/jimm.go index 35c9c71dd..4481e5ee5 100644 --- a/internal/jimm/jimm.go +++ b/internal/jimm/jimm.go @@ -10,8 +10,8 @@ import ( "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" - jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/juju/core/crossmodel" + jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/names/v4" "github.com/juju/zaputil/zapctx" "go.uber.org/zap" @@ -260,6 +260,17 @@ type API interface { // WatchAllModels creates a megawatcher. WatchAllModels(context.Context) (string, error) + + // ListFilesystems lists filesystems for desired machines. + // If no machines provided, a list of all filesystems is returned. + ListFilesystems(ctx context.Context, machines []string) ([]jujuparams.FilesystemDetailsListResult, error) + + // ListVolumes lists volumes for desired machines. + // If no machines provided, a list of all volumes is returned. + ListVolumes(ctx context.Context, machines []string) ([]jujuparams.VolumeDetailsListResult, error) + + // ListStorageDetails lists all storage. + ListStorageDetails(ctx context.Context) ([]jujuparams.StorageDetails, error) } // forEachController runs a given function on multiple controllers From 8b53ebeb19f35bf3cbbf9eabdd228a1706edf5c9 Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 21 Jun 2023 11:07:56 +0100 Subject: [PATCH 03/12] Bring in storage facade calls --- internal/jimm/model_status_parser_test.go | 818 ++++++++++++++++++++++ internal/jujuclient/storage.go | 111 +++ internal/jujuclient/storage_test.go | 154 ++++ 3 files changed, 1083 insertions(+) create mode 100644 internal/jimm/model_status_parser_test.go create mode 100644 internal/jujuclient/storage.go create mode 100644 internal/jujuclient/storage_test.go diff --git a/internal/jimm/model_status_parser_test.go b/internal/jimm/model_status_parser_test.go new file mode 100644 index 000000000..7f8ca7e57 --- /dev/null +++ b/internal/jimm/model_status_parser_test.go @@ -0,0 +1,818 @@ +package jimm_test + +import ( + "context" + "testing" + "time" + + qt "github.com/frankban/quicktest" + "github.com/google/uuid" + "github.com/juju/juju/core/life" + "github.com/juju/juju/core/status" + jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/names/v4" + + "github.com/CanonicalLtd/jimm/internal/db" + "github.com/CanonicalLtd/jimm/internal/dbmodel" + "github.com/CanonicalLtd/jimm/internal/errors" + "github.com/CanonicalLtd/jimm/internal/jimm" + "github.com/CanonicalLtd/jimm/internal/jimmtest" + "github.com/CanonicalLtd/jimm/internal/openfga" + ofga "github.com/CanonicalLtd/jimm/internal/openfga" + ofganames "github.com/CanonicalLtd/jimm/internal/openfga/names" + jimmnames "github.com/CanonicalLtd/jimm/pkg/names" +) + +var now = (time.Time{}).UTC().Round(time.Millisecond) + +const crossModelQueryEnv = ` +clouds: +- name: test-cloud + type: test-provider + regions: + - name: test-cloud-region +cloud-credentials: +- owner: alice@external + name: cred-1 + cloud: test-cloud +users: +- username: alice@external + controller-access: superuser +controllers: +- name: controller-1 + uuid: 10000000-0000-0000-0000-000000000000 + cloud: test-cloud + region: test-cloud-region +models: +- name: model-1 + type: iaas + uuid: 10000000-0000-0000-0000-000000000000 + controller: controller-1 + default-series: warty + cloud: test-cloud + region: test-cloud-region + cloud-credential: cred-1 + owner: alice@external + life: alive + status: + status: available + info: "OK!" + since: 2020-02-20T20:02:20Z +- name: model-2 + type: iaas + uuid: 20000000-0000-0000-0000-000000000000 + controller: controller-1 + default-series: warty + cloud: test-cloud + region: test-cloud-region + cloud-credential: cred-1 + owner: alice@external + life: alive + status: + status: available + info: "OK!" + since: 2020-02-20T20:02:20Z +- name: model-3 + type: iaas + uuid: 30000000-0000-0000-0000-000000000000 + controller: controller-1 + default-series: warty + cloud: test-cloud + region: test-cloud-region + cloud-credential: cred-1 + owner: alice@external + life: alive + status: + status: available + info: "OK!" + since: 2020-02-20T20:02:20Z +- name: model-5 + type: iaas + uuid: 50000000-0000-0000-0000-000000000000 + controller: controller-1 + default-series: warty + cloud: test-cloud + region: test-cloud-region + cloud-credential: cred-1 + owner: alice@external + life: alive + status: + status: available + info: "OK!" + since: 2020-02-20T20:02:20Z + users: + - user: alice@external + access: admin + sla: + level: unsupported + agent-version: 1.2.3 +` + +func getFullStatus( + modelName string, + applications map[string]jujuparams.ApplicationStatus, + remoteApps map[string]jujuparams.RemoteApplicationStatus, +) jujuparams.FullStatus { + return jujuparams.FullStatus{ + Model: jujuparams.ModelStatusInfo{ + Name: modelName, + Type: "caas", + CloudTag: "cloud-microk8s", + CloudRegion: "localhost", + Version: "2.9.37", + AvailableVersion: "", + ModelStatus: jujuparams.DetailedStatus{ + Status: "available", + Info: "", + Since: &now, + Data: map[string]interface{}{}, + }, + SLA: "unsupported", + }, + Machines: map[string]jujuparams.MachineStatus{}, + Applications: applications, + RemoteApplications: remoteApps, + Offers: map[string]jujuparams.ApplicationOfferStatus{}, + Relations: []jujuparams.RelationStatus(nil), + Branches: map[string]jujuparams.BranchStatus{}, + } +} + +// Model1 holds a model that is running against a K8S controller and has a single app +// waiting for a db relation to a cross-model endpoint via offer. +var model1 = getFullStatus("model-1", map[string]jujuparams.ApplicationStatus{ + "myapp": { + Charm: "myapp", + // handle charm-origin/name/rev/channel alex, + Scale: 1, + ProviderId: "10000000-0000-0000-0000-000000000000", + PublicAddress: "10.152.183.177", + Exposed: false, + Status: jujuparams.DetailedStatus{ // todo handle this alex + Status: "idle", + Info: "", + Since: &now, + }, + Relations: map[string][]string{ + "db": {"myapp"}, + }, + Units: map[string]jujuparams.UnitStatus{ + "myapp/0": { + AgentStatus: jujuparams.DetailedStatus{ + Status: "idle", + Version: "2.9.37", + Since: &now, + }, + WorkloadStatus: jujuparams.DetailedStatus{ + Status: "blocked", + Info: "waiting for db relation", + Since: &now, + }, + Leader: true, + Address: "10.1.160.61", + ProviderId: "myapp-0", + }, + }, + EndpointBindings: map[string]string{ + "": "alpha", + "db": "alpha", + "ingress": "alpha", + "myapp": "alpha", + }, + }, +}, + map[string]jujuparams.RemoteApplicationStatus{ + "postgresql": { + OfferURL: "lxdcloud:admin/db.postgresql", + Endpoints: []jujuparams.RemoteEndpoint{ + { + Name: "db", + Interface: "pgsql", + Role: "provider", + }, + }, + Status: jujuparams.DetailedStatus{ + Status: "active", + Info: "Live master (12.14)", + Since: &now, + }, + }, + }, +) + +// Model2 holds a model that is running against a K8S controller and has two apps. +// One that is currently deploying. It's unit status' are waiting on the agent installing. +// Another which is the ingress for this app. +// These apps have not been related. +// +// TODO(ale8k): How do we simulate storage? As this is just status... See newstatusformatter line 62, so we can test the below. +// Additionally, it has persistent volumes (filesystem). +// See: https://warthogs.atlassian.net/browse/CSS-3478 +var model2 = getFullStatus("model-2", map[string]jujuparams.ApplicationStatus{ + "hello-kubecon": { + Charm: "hello-kubecon", + Scale: 1, + CharmVersion: "17", + CharmChannel: "idk", + CharmProfile: "idk", + ProviderId: "20000000-0000-0000-0000-000000000000", + PublicAddress: "10.152.183.177", + Exposed: false, + Status: jujuparams.DetailedStatus{ // todo handle this alex + Status: "waiting", + Info: "installing agent", + Since: &now, + }, + Relations: map[string][]string{}, + Units: map[string]jujuparams.UnitStatus{ + "hello-kubecon/0": { + AgentStatus: jujuparams.DetailedStatus{ + Status: "allocating", + Version: "2.9.37", + Since: &now, + }, + WorkloadStatus: jujuparams.DetailedStatus{ + Status: "waiting", + Info: "installing agent", + Since: &now, + }, + Leader: true, + Address: "10.1.160.62", + ProviderId: "hello-kubecon-0", + }, + }, + EndpointBindings: map[string]string{ + "": "alpha", + "ingress": "alpha", + }, + }, + "nginx-ingress-integrator": { + Charm: "nginx-ingress-integrator", + Scale: 1, + CharmVersion: "54", + CharmChannel: "idk", + CharmProfile: "idk", + ProviderId: "20000000-0000-0000-0000-000000000000", + PublicAddress: "10.152.183.167", + Exposed: true, + Status: jujuparams.DetailedStatus{ // todo handle this alex + Status: "active", + Since: &now, + }, + Relations: map[string][]string{}, + Units: map[string]jujuparams.UnitStatus{ + "nginx-ingress-integrator/0": { + AgentStatus: jujuparams.DetailedStatus{ + Status: "idle", + Version: "2.9.37", + Since: &now, + }, + WorkloadStatus: jujuparams.DetailedStatus{ + Status: "active", + Since: &now, + }, + Leader: true, + Address: "10.1.160.63", + ProviderId: "nginx-ingress-integrator-0", + }, + }, + EndpointBindings: map[string]string{ + "": "alpha", + "ingress": "alpha", + }, + }, +}, + nil, +) + +// Model3 holds an empty model +var model3 = getFullStatus("model-3", map[string]jujuparams.ApplicationStatus{}, + nil, +) + +// Model5 holds an empty model, but it's API returns an error for storage +var model5 = getFullStatus("model-5", map[string]jujuparams.ApplicationStatus{}, + nil, +) + +func TestQueryModelsJq(t *testing.T) { + c := qt.New(t) + ctx := context.Background() + + // Test setup + _, client, _, err := jimmtest.SetupTestOFGAClient(c.Name()) + c.Assert(err, qt.IsNil) + + j := &jimm.JIMM{ + UUID: uuid.NewString(), + Database: db.Database{ + DB: jimmtest.MemoryDB(c, func() time.Time { return now }), + }, + OpenFGAClient: client, + Dialer: jimmtest.ModelDialerMap{ + "10000000-0000-0000-0000-000000000000": &jimmtest.Dialer{ + API: &jimmtest.API{ + Status_: func(_ context.Context, _ []string) (*jujuparams.FullStatus, error) { + return &model1, nil + }, + ListFilesystems_: func(ctx context.Context, machines []string) ([]jujuparams.FilesystemDetailsListResult, error) { + return []jujuparams.FilesystemDetailsListResult{ + { + Result: []jujuparams.FilesystemDetails{ + { + FilesystemTag: "filesystem-myapp-0-0", + VolumeTag: "volume-myapp-0-0", + Info: jujuparams.FilesystemInfo{ + Size: 4096, + Pool: "pool-1", + FilesystemId: "da64ec3c-0cf7-42f2-9951-35a5a3eaadc1", + }, + Life: life.Alive, + Status: jujuparams.EntityStatus{ + Status: status.Active, + Since: &now, + }, + UnitAttachments: map[string]jujuparams.FilesystemAttachmentDetails{ + "filesystem-myapp-0-1": { + FilesystemAttachmentInfo: jujuparams.FilesystemAttachmentInfo{ + MountPoint: "/home/ubuntu/myapp/.data", + ReadOnly: false, + }, + Life: "alive", + }, + }, + }, + }, + }, + }, nil + }, + ListVolumes_: func(ctx context.Context, machines []string) ([]jujuparams.VolumeDetailsListResult, error) { + return []jujuparams.VolumeDetailsListResult{}, nil + }, + ListStorageDetails_: func(ctx context.Context) ([]jujuparams.StorageDetails, error) { + return []jujuparams.StorageDetails{}, nil + }, + }, + }, + "20000000-0000-0000-0000-000000000000": &jimmtest.Dialer{ + API: &jimmtest.API{ + Status_: func(_ context.Context, _ []string) (*jujuparams.FullStatus, error) { + return &model2, nil + }, + ListFilesystems_: func(ctx context.Context, machines []string) ([]jujuparams.FilesystemDetailsListResult, error) { + return []jujuparams.FilesystemDetailsListResult{}, nil + }, + ListVolumes_: func(ctx context.Context, machines []string) ([]jujuparams.VolumeDetailsListResult, error) { + return []jujuparams.VolumeDetailsListResult{}, nil + }, + ListStorageDetails_: func(ctx context.Context) ([]jujuparams.StorageDetails, error) { + return []jujuparams.StorageDetails{}, nil + }, + }, + }, + "30000000-0000-0000-0000-000000000000": &jimmtest.Dialer{ + API: &jimmtest.API{ + Status_: func(_ context.Context, _ []string) (*jujuparams.FullStatus, error) { + return &model3, nil + }, + ListFilesystems_: func(ctx context.Context, machines []string) ([]jujuparams.FilesystemDetailsListResult, error) { + return []jujuparams.FilesystemDetailsListResult{}, nil + }, + ListVolumes_: func(ctx context.Context, machines []string) ([]jujuparams.VolumeDetailsListResult, error) { + return []jujuparams.VolumeDetailsListResult{}, nil + }, + ListStorageDetails_: func(ctx context.Context) ([]jujuparams.StorageDetails, error) { + return []jujuparams.StorageDetails{}, nil + }, + }, + }, + "50000000-0000-0000-0000-000000000000": &jimmtest.Dialer{ + API: &jimmtest.API{ + Status_: func(_ context.Context, _ []string) (*jujuparams.FullStatus, error) { + return &model5, nil + }, + ListFilesystems_: func(ctx context.Context, machines []string) ([]jujuparams.FilesystemDetailsListResult, error) { + return []jujuparams.FilesystemDetailsListResult{}, errors.E("forcing an error on model 5") + }, + ListVolumes_: func(ctx context.Context, machines []string) ([]jujuparams.VolumeDetailsListResult, error) { + return []jujuparams.VolumeDetailsListResult{}, nil + }, + ListStorageDetails_: func(ctx context.Context) ([]jujuparams.StorageDetails, error) { + return []jujuparams.StorageDetails{}, nil + }, + }, + }, + }, + } + + err = j.Database.Migrate(ctx, false) + c.Assert(err, qt.IsNil) + + env := jimmtest.ParseEnvironment(c, crossModelQueryEnv) + env.PopulateDB(c, j.Database, nil) + + modelUUIDs := []string{ + "10000000-0000-0000-0000-000000000000", + "20000000-0000-0000-0000-000000000000", + "30000000-0000-0000-0000-000000000000", + "40000000-0000-0000-0000-000000000000", // Erroneous model (doesn't exist). + "50000000-0000-0000-0000-000000000000", // Erroneous model (storage errors). + } + + c.Assert(j.OpenFGAClient.AddRelations(ctx, + []ofga.Tuple{ + // Reader to model via direct relation + { + Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Relation: ofganames.ReaderRelation, + Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[0])), + }, + { + Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Relation: ofganames.ReaderRelation, + Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[4])), + }, + // Reader to model via group + { + Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Relation: ofganames.MemberRelation, + Target: ofganames.ConvertTag(jimmnames.NewGroupTag("1")), + }, + { + Object: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag("1"), ofganames.MemberRelation), + Relation: ofganames.ReaderRelation, + Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[1])), + }, + // Reader to model via administrator of controller + { + Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Relation: ofganames.AdministratorRelation, + Target: ofganames.ConvertTag(names.NewControllerTag("00000000-0000-0000-0000-000000000000")), + }, + { + Object: ofganames.ConvertTag(names.NewControllerTag("00000000-0000-0000-0000-000000000000")), + Relation: ofganames.ControllerRelation, + Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[2])), + }, + // Reader to model via direct relation that does NOT exist + { + Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), + Relation: ofganames.ReaderRelation, + Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[3])), + }, + }..., + ), qt.Equals, nil) + + alice := openfga.NewUser( + &dbmodel.User{ + Username: "alice@external", + }, + client, + ) + + // Tests: + + // Query for all models only. + userModelUUIDs, err := alice.ListModels(ctx) + c.Assert(err, qt.IsNil) + + res, err := j.QueryModelsJq(ctx, userModelUUIDs, ".model") + c.Assert(err, qt.IsNil) + c.Assert(` + { + "results": { + "10000000-0000-0000-0000-000000000000": [ + { + "cloud": "microk8s", + "controller": "controller-1", + "model-status": { + "current": "available", + "since": "0001-01-01 00:00:00Z" + }, + "name": "model-1", + "region": "localhost", + "sla": "unsupported", + "type": "caas", + "version": "2.9.37" + } + ], + "20000000-0000-0000-0000-000000000000": [ + { + "cloud": "microk8s", + "controller": "controller-1", + "model-status": { + "current": "available", + "since": "0001-01-01 00:00:00Z" + }, + "name": "model-2", + "region": "localhost", + "sla": "unsupported", + "type": "caas", + "version": "2.9.37" + } + ], + "30000000-0000-0000-0000-000000000000": [ + { + "cloud": "microk8s", + "controller": "controller-1", + "model-status": { + "current": "available", + "since": "0001-01-01 00:00:00Z" + }, + "name": "model-3", + "region": "localhost", + "sla": "unsupported", + "type": "caas", + "version": "2.9.37" + } + ] + }, + "errors": { + "40000000-0000-0000-0000-000000000000": [ + "model not found" + ], + "50000000-0000-0000-0000-000000000000": [ + "forcing an error on model 5" + ] + } + } + `, qt.JSONEquals, res) + + // Query all applications across all models. + userModelUUIDs, err = alice.ListModels(ctx) + c.Assert(err, qt.IsNil) + + res, err = j.QueryModelsJq(ctx, userModelUUIDs, ".applications") + c.Assert(err, qt.IsNil) + c.Assert(` + { + "results": { + "10000000-0000-0000-0000-000000000000": [ + { + "myapp": { + "address": "10.152.183.177", + "application-status": { + "current": "idle", + "since": "0001-01-01 00:00:00Z" + }, + "charm": "myapp", + "charm-name": "myapp", + "charm-origin": "charmhub", + "charm-rev": -1, + "endpoint-bindings": { + "": "alpha", + "db": "alpha", + "ingress": "alpha", + "myapp": "alpha" + }, + "exposed": false, + "provider-id": "10000000-0000-0000-0000-000000000000", + "relations": { + "db": [ + "myapp" + ] + }, + "scale": 1, + "units": { + "myapp/0": { + "address": "10.1.160.61", + "juju-status": { + "current": "idle", + "since": "0001-01-01 00:00:00Z", + "version": "2.9.37" + }, + "leader": true, + "provider-id": "myapp-0", + "workload-status": { + "current": "blocked", + "message": "waiting for db relation", + "since": "0001-01-01 00:00:00Z" + } + } + } + } + } + ], + "20000000-0000-0000-0000-000000000000": [ + { + "hello-kubecon": { + "address": "10.152.183.177", + "application-status": { + "current": "waiting", + "message": "installing agent", + "since": "0001-01-01 00:00:00Z" + }, + "charm": "hello-kubecon", + "charm-channel": "idk", + "charm-name": "hello-kubecon", + "charm-origin": "charmhub", + "charm-profile": "idk", + "charm-rev": -1, + "charm-version": "17", + "endpoint-bindings": { + "": "alpha", + "ingress": "alpha" + }, + "exposed": false, + "provider-id": "20000000-0000-0000-0000-000000000000", + "scale": 1, + "units": { + "hello-kubecon/0": { + "address": "10.1.160.62", + "juju-status": { + "current": "allocating", + "since": "0001-01-01 00:00:00Z", + "version": "2.9.37" + }, + "leader": true, + "provider-id": "hello-kubecon-0", + "workload-status": { + "current": "waiting", + "message": "installing agent", + "since": "0001-01-01 00:00:00Z" + } + } + } + }, + "nginx-ingress-integrator": { + "address": "10.152.183.167", + "application-status": { + "current": "active", + "since": "0001-01-01 00:00:00Z" + }, + "charm": "nginx-ingress-integrator", + "charm-channel": "idk", + "charm-name": "nginx-ingress-integrator", + "charm-origin": "charmhub", + "charm-profile": "idk", + "charm-rev": -1, + "charm-version": "54", + "endpoint-bindings": { + "": "alpha", + "ingress": "alpha" + }, + "exposed": true, + "provider-id": "20000000-0000-0000-0000-000000000000", + "scale": 1, + "units": { + "nginx-ingress-integrator/0": { + "address": "10.1.160.63", + "juju-status": { + "current": "idle", + "since": "0001-01-01 00:00:00Z", + "version": "2.9.37" + }, + "leader": true, + "provider-id": "nginx-ingress-integrator-0", + "workload-status": { + "current": "active", + "since": "0001-01-01 00:00:00Z" + } + } + } + } + } + ], + "30000000-0000-0000-0000-000000000000": [ + {} + ] + }, + "errors": { + "40000000-0000-0000-0000-000000000000": [ + "model not found" + ], + "50000000-0000-0000-0000-000000000000": [ + "forcing an error on model 5" + ] + } + } + `, qt.JSONEquals, res) + + // Query specifically for models including the app "nginx-ingress-integrator" + userModelUUIDs, err = alice.ListModels(ctx) + c.Assert(err, qt.IsNil) + + res, err = j.QueryModelsJq(ctx, userModelUUIDs, ".applications | with_entries(select(.key==\"nginx-ingress-integrator\"))") + c.Assert(err, qt.IsNil) + c.Assert(` + { + "results": { + "10000000-0000-0000-0000-000000000000": [ + {} + ], + "20000000-0000-0000-0000-000000000000": [ + { + "nginx-ingress-integrator": { + "address": "10.152.183.167", + "application-status": { + "current": "active", + "since": "0001-01-01 00:00:00Z" + }, + "charm": "nginx-ingress-integrator", + "charm-channel": "idk", + "charm-name": "nginx-ingress-integrator", + "charm-origin": "charmhub", + "charm-profile": "idk", + "charm-rev": -1, + "charm-version": "54", + "endpoint-bindings": { + "": "alpha", + "ingress": "alpha" + }, + "exposed": true, + "provider-id": "20000000-0000-0000-0000-000000000000", + "scale": 1, + "units": { + "nginx-ingress-integrator/0": { + "address": "10.1.160.63", + "juju-status": { + "current": "idle", + "since": "0001-01-01 00:00:00Z", + "version": "2.9.37" + }, + "leader": true, + "provider-id": "nginx-ingress-integrator-0", + "workload-status": { + "current": "active", + "since": "0001-01-01 00:00:00Z" + } + } + } + } + } + ], + "30000000-0000-0000-0000-000000000000": [ + {} + ] + }, + "errors": { + "40000000-0000-0000-0000-000000000000": [ + "model not found" + ], + "50000000-0000-0000-0000-000000000000": [ + "forcing an error on model 5" + ] + } + } + `, qt.JSONEquals, res) + + // Query specifically for storage on this model. + userModelUUIDs, err = alice.ListModels(ctx) + c.Assert(err, qt.IsNil) + + res, err = j.QueryModelsJq(ctx, userModelUUIDs, ".storage") + c.Assert(err, qt.IsNil) + + // Not the cleanest thing in the world, but this field needs ignoring, + // and as our struct has a nested map, cmpopts.IgnoreMapFields won't do. + res. + Results[modelUUIDs[0]][0].(map[string]any)["filesystems"].(map[string]any)["myapp/0/0"].(map[string]any)["status"].(map[string]any)["since"] = "" + + c.Assert(` + { + "results": { + "10000000-0000-0000-0000-000000000000": [ + { + "filesystems": { + "myapp/0/0": { + "Attachments": { + "containers": { + "myapp/0/1": { + "life": "alive", + "mount-point": "/home/ubuntu/myapp/.data", + "read-only": false + } + } + }, + "life": "alive", + "pool": "pool-1", + "provider-id": "da64ec3c-0cf7-42f2-9951-35a5a3eaadc1", + "size": 4096, + "status": { + "current": "active", + "since": "" + }, + "volume": "myapp/0/0" + } + } + } + ], + "20000000-0000-0000-0000-000000000000": [ + {} + ], + "30000000-0000-0000-0000-000000000000": [ + {} + ] + }, + "errors": { + "40000000-0000-0000-0000-000000000000": [ + "model not found" + ], + "50000000-0000-0000-0000-000000000000": [ + "forcing an error on model 5" + ] + } + } + `, qt.JSONEquals, res) + +} diff --git a/internal/jujuclient/storage.go b/internal/jujuclient/storage.go new file mode 100644 index 000000000..26ddf5b5e --- /dev/null +++ b/internal/jujuclient/storage.go @@ -0,0 +1,111 @@ +// Copyright 2023 Canonical Ltd. + +package jujuclient + +import ( + "context" + + jujuerrors "github.com/juju/errors" + jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/names/v4" + + "github.com/CanonicalLtd/jimm/internal/errors" +) + +// ListFilesystems lists filesystems for desired machines. +// If no machines provided, a list of all filesystems is returned. +func (c Connection) ListFilesystems(ctx context.Context, machines []string) ([]jujuparams.FilesystemDetailsListResult, error) { + const op = errors.Op("jujuclient.ListFilesystems") + + filters := make([]jujuparams.FilesystemFilter, len(machines)) + for i, machine := range machines { + filters[i].Machines = []string{names.NewMachineTag(machine).String()} + } + if len(filters) == 0 { + filters = []jujuparams.FilesystemFilter{{}} + } + + args := jujuparams.FilesystemFilters{ + Filters: filters, + } + var results jujuparams.FilesystemDetailsListResults + + if err := c.CallHighestFacadeVersion(ctx, "Storage", []int{6}, "", "ListFilesystems", &args, &results); err != nil { + return nil, errors.E(op, jujuerrors.Cause(err)) + } + + if len(results.Results) != len(filters) { + return nil, errors.E( + op, + jujuerrors.Errorf( + "expected %d result(s), got %d", + len(filters), len(results.Results), + ), + ) + } + + return results.Results, nil +} + +// ListVolumes lists volumes for desired machines. +// If no machines provided, a list of all volumes is returned. +func (c Connection) ListVolumes(ctx context.Context, machines []string) ([]jujuparams.VolumeDetailsListResult, error) { + const op = errors.Op("jujuclient.ListVolumes") + + filters := make([]jujuparams.VolumeFilter, len(machines)) + for i, machine := range machines { + filters[i].Machines = []string{names.NewMachineTag(machine).String()} + } + if len(filters) == 0 { + filters = []jujuparams.VolumeFilter{{}} + } + args := jujuparams.VolumeFilters{Filters: filters} + var results jujuparams.VolumeDetailsListResults + + if err := c.CallHighestFacadeVersion(ctx, "Storage", []int{6}, "", "ListVolumes", &args, &results); err != nil { + return nil, errors.E(op, jujuerrors.Cause(err)) + } + + if len(results.Results) != len(filters) { + return nil, errors.E( + op, + jujuerrors.Errorf( + "expected %d result(s), got %d", + len(filters), len(results.Results), + ), + ) + } + + return results.Results, nil +} + +// ListStorageDetails lists all storage. +func (c Connection) ListStorageDetails(ctx context.Context) ([]jujuparams.StorageDetails, error) { + const op = errors.Op("jujuclient.ListStorageDetails") + + args := jujuparams.StorageFilters{ + Filters: []jujuparams.StorageFilter{{}}, // one empty filter + } + var results jujuparams.StorageDetailsListResults + + if err := c.CallHighestFacadeVersion(ctx, "Storage", []int{6}, "", "ListStorageDetails", &args, &results); err != nil { + return nil, errors.E(op, jujuerrors.Cause(err)) + } + + if len(results.Results) != 1 { + return nil, errors.E( + op, + jujuerrors.Errorf( + "expected 1 result, got %d", + len(results.Results), + ), + ) + } + if results.Results[0].Error != nil { + return nil, errors.E( + op, + jujuerrors.Trace(results.Results[0].Error), + ) + } + return results.Results[0].Result, nil +} diff --git a/internal/jujuclient/storage_test.go b/internal/jujuclient/storage_test.go new file mode 100644 index 000000000..ec286dacf --- /dev/null +++ b/internal/jujuclient/storage_test.go @@ -0,0 +1,154 @@ +// Copyright 2023 Canonical Ltd. +package jujuclient_test + +import ( + "context" + + jujuparams "github.com/juju/juju/rpc/params" + "github.com/juju/names/v4" + gc "gopkg.in/check.v1" + + "github.com/CanonicalLtd/jimm/internal/dbmodel" + "github.com/CanonicalLtd/jimm/internal/jimmtest" +) + +type storageSuite struct { + jujuclientSuite +} + +var _ = gc.Suite(&storageSuite{}) + +func (s *storageSuite) TestListFilesystems(c *gc.C) { + ctx := context.Background() + + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/pw1").String() + cred := jujuparams.TaggedCredential{ + Tag: cct, + Credential: jujuparams.CloudCredential{ + AuthType: "userpass", + Attributes: map[string]string{ + "username": "alibaba", + "password": "open sesame", + }, + }, + } + + info := s.APIInfo(c) + ctl := dbmodel.Controller{ + Name: s.ControllerConfig.ControllerName(), + CACertificate: info.CACert, + AdminUser: info.Tag.Id(), + AdminPassword: info.Password, + PublicAddress: info.Addrs[0], + } + + models, err := s.API.UpdateCredential(ctx, cred) + c.Assert(err, gc.Equals, nil) + c.Assert(models, gc.HasLen, 0) + + var modelInfo jujuparams.ModelInfo + err = s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ + Name: "model-1", + OwnerTag: names.NewUserTag("bob@external").String(), + CloudCredentialTag: cct, + }, &modelInfo) + c.Assert(err, gc.Equals, nil) + uuid := modelInfo.UUID + + api, err := s.Dialer.Dial(context.Background(), &ctl, names.NewModelTag(uuid)) + c.Assert(err, gc.IsNil) + _, err = api.ListFilesystems(ctx, nil) + c.Assert(err, gc.IsNil) + // TODO(ale8k): figure out how to add storage to mock models and check res after it + // for now this just tests the facade is called correctly I guess. +} + +func (s *storageSuite) TestListVolumes(c *gc.C) { + ctx := context.Background() + + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/pw1").String() + cred := jujuparams.TaggedCredential{ + Tag: cct, + Credential: jujuparams.CloudCredential{ + AuthType: "userpass", + Attributes: map[string]string{ + "username": "alibaba", + "password": "open sesame", + }, + }, + } + + info := s.APIInfo(c) + ctl := dbmodel.Controller{ + Name: s.ControllerConfig.ControllerName(), + CACertificate: info.CACert, + AdminUser: info.Tag.Id(), + AdminPassword: info.Password, + PublicAddress: info.Addrs[0], + } + + models, err := s.API.UpdateCredential(ctx, cred) + c.Assert(err, gc.Equals, nil) + c.Assert(models, gc.HasLen, 0) + + var modelInfo jujuparams.ModelInfo + err = s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ + Name: "model-1", + OwnerTag: names.NewUserTag("bob@external").String(), + CloudCredentialTag: cct, + }, &modelInfo) + c.Assert(err, gc.Equals, nil) + uuid := modelInfo.UUID + + api, err := s.Dialer.Dial(context.Background(), &ctl, names.NewModelTag(uuid)) + c.Assert(err, gc.IsNil) + _, err = api.ListVolumes(ctx, nil) + c.Assert(err, gc.IsNil) + // TODO(ale8k): figure out how to add storage to mock models and check res after it + // for now this just tests the facade is called correctly I guess. +} + +func (s *storageSuite) TestListStorageDetails(c *gc.C) { + ctx := context.Background() + + cct := names.NewCloudCredentialTag(jimmtest.TestCloudName + "/bob@external/pw1").String() + cred := jujuparams.TaggedCredential{ + Tag: cct, + Credential: jujuparams.CloudCredential{ + AuthType: "userpass", + Attributes: map[string]string{ + "username": "alibaba", + "password": "open sesame", + }, + }, + } + + info := s.APIInfo(c) + ctl := dbmodel.Controller{ + Name: s.ControllerConfig.ControllerName(), + CACertificate: info.CACert, + AdminUser: info.Tag.Id(), + AdminPassword: info.Password, + PublicAddress: info.Addrs[0], + } + + models, err := s.API.UpdateCredential(ctx, cred) + c.Assert(err, gc.Equals, nil) + c.Assert(models, gc.HasLen, 0) + + var modelInfo jujuparams.ModelInfo + err = s.API.CreateModel(ctx, &jujuparams.ModelCreateArgs{ + Name: "model-1", + OwnerTag: names.NewUserTag("bob@external").String(), + CloudCredentialTag: cct, + }, &modelInfo) + c.Assert(err, gc.Equals, nil) + uuid := modelInfo.UUID + + api, err := s.Dialer.Dial(context.Background(), &ctl, names.NewModelTag(uuid)) + c.Assert(err, gc.IsNil) + _, err = api.ListStorageDetails(ctx) + c.Assert(err, gc.IsNil) + // TODO(ale8k): figure out how to add storage to mock models and check res after it + // for now this just tests the facade is called correctly I guess. +} From 8b74e4d4d2420fc8e3f68a5aa58fb67c3a00a320 Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 21 Jun 2023 11:10:31 +0100 Subject: [PATCH 04/12] Update jimmtest api --- internal/jimmtest/api.go | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/internal/jimmtest/api.go b/internal/jimmtest/api.go index d012e358f..fb4eb33cc 100644 --- a/internal/jimmtest/api.go +++ b/internal/jimmtest/api.go @@ -9,8 +9,8 @@ import ( "time" "github.com/go-macaroon-bakery/macaroon-bakery/v3/bakery" - jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/juju/core/crossmodel" + jujuparams "github.com/juju/juju/rpc/params" "github.com/juju/juju/version" "github.com/juju/names/v4" @@ -86,11 +86,23 @@ func (w apiWrapper) Close() error { return w.API.Close() } +// ModelDialerMap enables the dialing of many models on the same controller, +// it is designed such that should you need to query multiple models, you can. +type ModelDialerMap map[string]jimm.Dialer + +// Dial implements jimm.Dialer. +func (m ModelDialerMap) Dial(ctx context.Context, ctl *dbmodel.Controller, mt names.ModelTag) (jimm.API, error) { + if d, ok := m[mt.Id()]; ok { + return d.Dial(ctx, ctl, mt) + } + return nil, errors.E(fmt.Sprintf("dialer not configured for controller %s", ctl.Name)) +} + // A DialerMap implements a jimm.Dialer that uses a different Dialer for // each controller. The DialerMap is keyed by controller name. type DialerMap map[string]jimm.Dialer -// Dialer implements jimm.Dialer. +// Dial implements jimm.Dialer. func (m DialerMap) Dial(ctx context.Context, ctl *dbmodel.Controller, mt names.ModelTag) (jimm.API, error) { if d, ok := m[ctl.Name]; ok { return d.Dial(ctx, ctl, mt) @@ -149,6 +161,9 @@ type API struct { WatchAll_ func(context.Context) (string, error) WatchAllModelSummaries_ func(context.Context) (string, error) WatchAllModels_ func(context.Context) (string, error) + ListFilesystems_ func(ctx context.Context, machines []string) ([]jujuparams.FilesystemDetailsListResult, error) + ListVolumes_ func(ctx context.Context, machines []string) ([]jujuparams.VolumeDetailsListResult, error) + ListStorageDetails_ func(ctx context.Context) ([]jujuparams.StorageDetails, error) } func (a *API) AddCloud(ctx context.Context, tag names.CloudTag, cld jujuparams.Cloud) error { @@ -464,4 +479,25 @@ func (a *API) WatchAll(ctx context.Context) (string, error) { return a.WatchAll_(ctx) } +func (a *API) ListFilesystems(ctx context.Context, machines []string) ([]jujuparams.FilesystemDetailsListResult, error) { + if a.ListFilesystems_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return a.ListFilesystems_(ctx, machines) +} + +func (a *API) ListVolumes(ctx context.Context, machines []string) ([]jujuparams.VolumeDetailsListResult, error) { + if a.ListVolumes_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return a.ListVolumes_(ctx, machines) +} + +func (a *API) ListStorageDetails(ctx context.Context) ([]jujuparams.StorageDetails, error) { + if a.ListStorageDetails_ == nil { + return nil, errors.E(errors.CodeNotImplemented) + } + return a.ListStorageDetails_(ctx) +} + var _ jimm.API = &API{} From d3f8d90d9b267d63f749aea66f3b3cf8b15e8329 Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 21 Jun 2023 11:19:21 +0100 Subject: [PATCH 05/12] Add api call --- api/jimm.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/api/jimm.go b/api/jimm.go index 1d2e25a09..9fb46a776 100644 --- a/api/jimm.go +++ b/api/jimm.go @@ -112,3 +112,12 @@ func (c *Client) ImportModel(req *params.ImportModelRequest) error { func (c *Client) UpdateMigratedModel(req *params.UpdateMigratedModelRequest) error { return c.caller.APICall("JIMM", 3, "", "UpdateMigratedModel", req, nil) } + +// CrossModelQuery enables users to query all of their available models and each entity within the model. +// +// The query will run against output exactly like "juju status --format json", but for each of their models. +func (c *Client) CrossModelQuery(req *params.CrossModelQueryRequest) (*params.CrossModelQueryResponse, error) { + var response params.CrossModelQueryResponse + err := c.caller.APICall("JIMM", 4, "", "CrossModelQuery", req, &response) + return &response, err +} From 83173bea8f275f3a306aea8ac2387aebfc40e790 Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 21 Jun 2023 11:19:48 +0100 Subject: [PATCH 06/12] Add API params --- api/params/params.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/api/params/params.go b/api/params/params.go index 93d94f737..918c2812a 100644 --- a/api/params/params.go +++ b/api/params/params.go @@ -237,3 +237,19 @@ type ImportModelRequest struct { // ModelTag is the tag of the model that is to be imported. ModelTag string `json:"model-tag"` } + +// CrossModelQueryRequest holds the parameters to perform a cross model query against +// JSON model statuses for every model this user has access to. +type CrossModelQueryRequest struct { + Type string `json:"type"` + Query string `json:"query"` +} + +// CrossModelJqQueryResponse holds results for a cross-model query that has been filtered utilising JQ. +// It has two fields: +// - Results - A map of each iterated JQ output result. The key for this map is the model UUID. +// - Errors - A map of each iterated JQ *or* Status call error. The key for this map is the model UUID. +type CrossModelQueryResponse struct { + Results map[string][]any `json:"results"` + Errors map[string][]string `json:"errors"` +} From 6fec97a789ddadc5a8aab310e334550de7bed996 Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 21 Jun 2023 11:43:37 +0100 Subject: [PATCH 07/12] Facade call --- internal/jujuapi/jimm.go | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/internal/jujuapi/jimm.go b/internal/jujuapi/jimm.go index dee0bac6d..8b51c11f2 100644 --- a/internal/jujuapi/jimm.go +++ b/internal/jujuapi/jimm.go @@ -4,6 +4,7 @@ package jujuapi import ( "context" + "strings" "time" "github.com/juju/juju/core/network" @@ -35,6 +36,7 @@ func init() { updateMigratedModelMethod := rpc.Method(r.UpdateMigratedModel) addCloudToControllerMethod := rpc.Method(r.AddCloudToController) removeCloudFromControllerMethod := rpc.Method(r.RemoveCloudFromController) + crossModelQueryMethod := rpc.Method(r.CrossModelQuery) r.AddMethod("JIMM", 2, "DisableControllerUUIDMasking", disableControllerUUIDMaskingMethod) r.AddMethod("JIMM", 2, "ListControllers", listControllersMethod) @@ -52,6 +54,7 @@ func init() { r.AddMethod("JIMM", 3, "UpdateMigratedModel", updateMigratedModelMethod) r.AddMethod("JIMM", 3, "AddCloudToController", addCloudToControllerMethod) r.AddMethod("JIMM", 3, "RemoveCloudFromController", removeCloudFromControllerMethod) + r.AddMethod("JIMM", 3, "CrossModelQuery", crossModelQueryMethod) return []int{2, 3} } @@ -441,3 +444,29 @@ func (r *controllerRoot) RemoveCloudFromController(ctx context.Context, req apip } return nil } + +// CrossModelQuery enables users to query all of their available models and each entity within the model. +// +// The query will run against output exactly like "juju status --format json", but for each of their models. +func (r *controllerRoot) CrossModelQuery(ctx context.Context, req apiparams.CrossModelQueryRequest) (apiparams.CrossModelQueryResponse, error) { + const op = errors.Op("jujuapi.CrossModelQuery") + + models, err := r.jimm.Database.GetUserModels(ctx, r.user) + modelUUIDs := make([]string, len(models)) + + for i, m := range models { + modelUUIDs[i] = m.Model_.UUID.String + } + + if err != nil { + return apiparams.CrossModelQueryResponse{}, errors.E(op, errors.Code("failed to get models for user")) + } + switch strings.TrimSpace(strings.ToLower(req.Type)) { + case "jq": + return r.jimm.QueryModelsJq(ctx, modelUUIDs, req.Query) + case "jimmsql": + return apiparams.CrossModelQueryResponse{}, errors.E(op, errors.CodeNotImplemented) + default: + return apiparams.CrossModelQueryResponse{}, errors.E(op, errors.Code("invalid query type"), "unable to query models") + } +} From 3914d64caf7374317fce6b2da6ca242bb2284ff1 Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 21 Jun 2023 13:46:13 +0100 Subject: [PATCH 08/12] Initial backport of parser --- internal/jimm/model_status_parser.go | 14 ++- internal/jimm/model_status_parser_test.go | 116 ++++++---------------- 2 files changed, 39 insertions(+), 91 deletions(-) diff --git a/internal/jimm/model_status_parser.go b/internal/jimm/model_status_parser.go index 6b7852f77..f5a4e5530 100644 --- a/internal/jimm/model_status_parser.go +++ b/internal/jimm/model_status_parser.go @@ -4,7 +4,6 @@ import ( "context" "database/sql" "encoding/json" - "strings" "github.com/CanonicalLtd/jimm/api/params" "github.com/CanonicalLtd/jimm/internal/dbmodel" @@ -16,6 +15,7 @@ import ( "github.com/juju/juju/cmd/juju/status" "github.com/juju/juju/cmd/juju/storage" rpcparams "github.com/juju/juju/rpc/params" + "github.com/juju/names/v4" "github.com/juju/zaputil/zapctx" ) @@ -39,9 +39,9 @@ func (j *JIMM) QueryModelsJq(ctx context.Context, modelUUIDs []string, jqQuery s // We remove "model:" from the UUIDs, unfortunately that's what OpenFGA returns now after // recent versions. - for i := range modelUUIDs { - modelUUIDs[i] = strings.Split(modelUUIDs[i], ":")[1] - } + // for i := range modelUUIDs { + // modelUUIDs[i] = strings.Split(modelUUIDs[i], ":")[1] + // } // Set up a formatterParamsRetriever to handle the heavy lifting // of each facade call and type conversion. @@ -171,7 +171,11 @@ func (f *formatterParamsRetriever) loadModel(ctx context.Context, modelUUID stri // dialModel dials the model currently loaded into the formatterParamsRetriever. func (f *formatterParamsRetriever) dialModel(ctx context.Context) error { - api, err := f.jimm.dial(ctx, &f.model.Controller, f.model.ResourceTag()) + modelTag, ok := f.model.Tag().(names.ModelTag) + if !ok { + return errors.E(errors.Op("failed to parse model tag")) + } + api, err := f.jimm.dial(ctx, &f.model.Controller, modelTag) if err != nil { zapctx.Error(ctx, "failed to dial controller for model", zap.String("controller-uuid", f.model.Controller.UUID), zap.String("model-uuid", f.model.UUID.String), zap.Error(err)) } diff --git a/internal/jimm/model_status_parser_test.go b/internal/jimm/model_status_parser_test.go index 7f8ca7e57..8ba5562a4 100644 --- a/internal/jimm/model_status_parser_test.go +++ b/internal/jimm/model_status_parser_test.go @@ -10,17 +10,11 @@ import ( "github.com/juju/juju/core/life" "github.com/juju/juju/core/status" jujuparams "github.com/juju/juju/rpc/params" - "github.com/juju/names/v4" "github.com/CanonicalLtd/jimm/internal/db" - "github.com/CanonicalLtd/jimm/internal/dbmodel" "github.com/CanonicalLtd/jimm/internal/errors" "github.com/CanonicalLtd/jimm/internal/jimm" "github.com/CanonicalLtd/jimm/internal/jimmtest" - "github.com/CanonicalLtd/jimm/internal/openfga" - ofga "github.com/CanonicalLtd/jimm/internal/openfga" - ofganames "github.com/CanonicalLtd/jimm/internal/openfga/names" - jimmnames "github.com/CanonicalLtd/jimm/pkg/names" ) var now = (time.Time{}).UTC().Round(time.Millisecond) @@ -58,6 +52,9 @@ models: status: available info: "OK!" since: 2020-02-20T20:02:20Z + users: + - user: alice@external + access: admin - name: model-2 type: iaas uuid: 20000000-0000-0000-0000-000000000000 @@ -72,6 +69,9 @@ models: status: available info: "OK!" since: 2020-02-20T20:02:20Z + users: + - user: alice@external + access: admin - name: model-3 type: iaas uuid: 30000000-0000-0000-0000-000000000000 @@ -86,6 +86,9 @@ models: status: available info: "OK!" since: 2020-02-20T20:02:20Z + users: + - user: alice@external + access: admin - name: model-5 type: iaas uuid: 50000000-0000-0000-0000-000000000000 @@ -299,16 +302,11 @@ func TestQueryModelsJq(t *testing.T) { c := qt.New(t) ctx := context.Background() - // Test setup - _, client, _, err := jimmtest.SetupTestOFGAClient(c.Name()) - c.Assert(err, qt.IsNil) - j := &jimm.JIMM{ UUID: uuid.NewString(), Database: db.Database{ DB: jimmtest.MemoryDB(c, func() time.Time { return now }), }, - OpenFGAClient: client, Dialer: jimmtest.ModelDialerMap{ "10000000-0000-0000-0000-000000000000": &jimmtest.Dialer{ API: &jimmtest.API{ @@ -405,76 +403,28 @@ func TestQueryModelsJq(t *testing.T) { }, } - err = j.Database.Migrate(ctx, false) + err := j.Database.Migrate(ctx, false) c.Assert(err, qt.IsNil) env := jimmtest.ParseEnvironment(c, crossModelQueryEnv) - env.PopulateDB(c, j.Database, nil) + env.PopulateDB(c, j.Database) + + user := env.User("alice@external").DBObject(c, j.Database) modelUUIDs := []string{ "10000000-0000-0000-0000-000000000000", "20000000-0000-0000-0000-000000000000", "30000000-0000-0000-0000-000000000000", - "40000000-0000-0000-0000-000000000000", // Erroneous model (doesn't exist). "50000000-0000-0000-0000-000000000000", // Erroneous model (storage errors). } - c.Assert(j.OpenFGAClient.AddRelations(ctx, - []ofga.Tuple{ - // Reader to model via direct relation - { - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), - Relation: ofganames.ReaderRelation, - Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[0])), - }, - { - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), - Relation: ofganames.ReaderRelation, - Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[4])), - }, - // Reader to model via group - { - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), - Relation: ofganames.MemberRelation, - Target: ofganames.ConvertTag(jimmnames.NewGroupTag("1")), - }, - { - Object: ofganames.ConvertTagWithRelation(jimmnames.NewGroupTag("1"), ofganames.MemberRelation), - Relation: ofganames.ReaderRelation, - Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[1])), - }, - // Reader to model via administrator of controller - { - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), - Relation: ofganames.AdministratorRelation, - Target: ofganames.ConvertTag(names.NewControllerTag("00000000-0000-0000-0000-000000000000")), - }, - { - Object: ofganames.ConvertTag(names.NewControllerTag("00000000-0000-0000-0000-000000000000")), - Relation: ofganames.ControllerRelation, - Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[2])), - }, - // Reader to model via direct relation that does NOT exist - { - Object: ofganames.ConvertTag(names.NewUserTag("alice@external")), - Relation: ofganames.ReaderRelation, - Target: ofganames.ConvertTag(names.NewModelTag(modelUUIDs[3])), - }, - }..., - ), qt.Equals, nil) - - alice := openfga.NewUser( - &dbmodel.User{ - Username: "alice@external", - }, - client, - ) - - // Tests: - // Query for all models only. - userModelUUIDs, err := alice.ListModels(ctx) + userModels, err := j.Database.GetUserModels(ctx, &user) c.Assert(err, qt.IsNil) + userModelUUIDs := make([]string, len(userModels)) + for i, m := range userModels { + userModelUUIDs[i] = m.Model_.UUID.String + } res, err := j.QueryModelsJq(ctx, userModelUUIDs, ".model") c.Assert(err, qt.IsNil) @@ -528,9 +478,6 @@ func TestQueryModelsJq(t *testing.T) { ] }, "errors": { - "40000000-0000-0000-0000-000000000000": [ - "model not found" - ], "50000000-0000-0000-0000-000000000000": [ "forcing an error on model 5" ] @@ -539,8 +486,10 @@ func TestQueryModelsJq(t *testing.T) { `, qt.JSONEquals, res) // Query all applications across all models. - userModelUUIDs, err = alice.ListModels(ctx) - c.Assert(err, qt.IsNil) + // Reset the model UUID for test (as it mutates the slice) + for i, m := range userModels { + userModelUUIDs[i] = m.Model_.UUID.String + } res, err = j.QueryModelsJq(ctx, userModelUUIDs, ".applications") c.Assert(err, qt.IsNil) @@ -678,9 +627,6 @@ func TestQueryModelsJq(t *testing.T) { ] }, "errors": { - "40000000-0000-0000-0000-000000000000": [ - "model not found" - ], "50000000-0000-0000-0000-000000000000": [ "forcing an error on model 5" ] @@ -689,8 +635,10 @@ func TestQueryModelsJq(t *testing.T) { `, qt.JSONEquals, res) // Query specifically for models including the app "nginx-ingress-integrator" - userModelUUIDs, err = alice.ListModels(ctx) - c.Assert(err, qt.IsNil) + // Reset the model UUID for test (as it mutates the slice) + for i, m := range userModels { + userModelUUIDs[i] = m.Model_.UUID.String + } res, err = j.QueryModelsJq(ctx, userModelUUIDs, ".applications | with_entries(select(.key==\"nginx-ingress-integrator\"))") c.Assert(err, qt.IsNil) @@ -746,9 +694,6 @@ func TestQueryModelsJq(t *testing.T) { ] }, "errors": { - "40000000-0000-0000-0000-000000000000": [ - "model not found" - ], "50000000-0000-0000-0000-000000000000": [ "forcing an error on model 5" ] @@ -757,8 +702,10 @@ func TestQueryModelsJq(t *testing.T) { `, qt.JSONEquals, res) // Query specifically for storage on this model. - userModelUUIDs, err = alice.ListModels(ctx) - c.Assert(err, qt.IsNil) + // Reset the model UUID for test (as it mutates the slice) + for i, m := range userModels { + userModelUUIDs[i] = m.Model_.UUID.String + } res, err = j.QueryModelsJq(ctx, userModelUUIDs, ".storage") c.Assert(err, qt.IsNil) @@ -805,9 +752,6 @@ func TestQueryModelsJq(t *testing.T) { ] }, "errors": { - "40000000-0000-0000-0000-000000000000": [ - "model not found" - ], "50000000-0000-0000-0000-000000000000": [ "forcing an error on model 5" ] From dac2853fbb76cc1a3ce263a2b9b69d9a4f6c224f Mon Sep 17 00:00:00 2001 From: ale8k Date: Wed, 21 Jun 2023 14:43:32 +0100 Subject: [PATCH 09/12] Fix relation test --- internal/jimm/model_status_parser_test.go | 32 ++++++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/internal/jimm/model_status_parser_test.go b/internal/jimm/model_status_parser_test.go index 8ba5562a4..f5518be10 100644 --- a/internal/jimm/model_status_parser_test.go +++ b/internal/jimm/model_status_parser_test.go @@ -115,6 +115,8 @@ func getFullStatus( modelName string, applications map[string]jujuparams.ApplicationStatus, remoteApps map[string]jujuparams.RemoteApplicationStatus, + modelRelations []jujuparams.RelationStatus, + ) jujuparams.FullStatus { return jujuparams.FullStatus{ Model: jujuparams.ModelStatusInfo{ @@ -136,7 +138,7 @@ func getFullStatus( Applications: applications, RemoteApplications: remoteApps, Offers: map[string]jujuparams.ApplicationOfferStatus{}, - Relations: []jujuparams.RelationStatus(nil), + Relations: modelRelations, Branches: map[string]jujuparams.BranchStatus{}, } } @@ -201,6 +203,22 @@ var model1 = getFullStatus("model-1", map[string]jujuparams.ApplicationStatus{ }, }, }, + []jujuparams.RelationStatus{ + { + Id: 0, + Key: "myapp", + Interface: "db", + Scope: "regular", + Endpoints: []jujuparams.EndpointStatus{ + { + ApplicationName: "myapp", + Name: "db", + Role: "myrole", + Subordinate: false, + }, + }, + }, + }, ) // Model2 holds a model that is running against a K8S controller and has two apps. @@ -286,16 +304,19 @@ var model2 = getFullStatus("model-2", map[string]jujuparams.ApplicationStatus{ }, }, nil, + nil, ) // Model3 holds an empty model var model3 = getFullStatus("model-3", map[string]jujuparams.ApplicationStatus{}, nil, + nil, ) // Model5 holds an empty model, but it's API returns an error for storage var model5 = getFullStatus("model-5", map[string]jujuparams.ApplicationStatus{}, nil, + nil, ) func TestQueryModelsJq(t *testing.T) { @@ -518,7 +539,11 @@ func TestQueryModelsJq(t *testing.T) { "provider-id": "10000000-0000-0000-0000-000000000000", "relations": { "db": [ - "myapp" + { + "interface": "db", + "related-application": "myapp", + "scope": "regular" + } ] }, "scale": 1, @@ -754,9 +779,8 @@ func TestQueryModelsJq(t *testing.T) { "errors": { "50000000-0000-0000-0000-000000000000": [ "forcing an error on model 5" - ] + ] } } `, qt.JSONEquals, res) - } From b536670db450ead779039d8de713e9d24087bfdf Mon Sep 17 00:00:00 2001 From: ale8k Date: Mon, 26 Jun 2023 11:46:50 +0100 Subject: [PATCH 10/12] PR comments --- internal/jimm/model_status_parser.go | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/internal/jimm/model_status_parser.go b/internal/jimm/model_status_parser.go index f5a4e5530..6524bdd47 100644 --- a/internal/jimm/model_status_parser.go +++ b/internal/jimm/model_status_parser.go @@ -19,7 +19,7 @@ import ( "github.com/juju/zaputil/zapctx" ) -// QueryModels queries every model available to a given user. +// QueryModels queries every specified model in modelUUIDs. // // The jqQuery must be a valid jq query and can return every result, even iterative listings. // If a result is erroneous, for example, bad data type parsing, the resulting struct field @@ -37,12 +37,6 @@ func (j *JIMM) QueryModelsJq(ctx context.Context, modelUUIDs []string, jqQuery s return results, errors.E(op, err) } - // We remove "model:" from the UUIDs, unfortunately that's what OpenFGA returns now after - // recent versions. - // for i := range modelUUIDs { - // modelUUIDs[i] = strings.Split(modelUUIDs[i], ":")[1] - // } - // Set up a formatterParamsRetriever to handle the heavy lifting // of each facade call and type conversion. retriever := newFormatterParamsRetriever(j) From 25f298e041e08069a71acd971ac7a8b2490367db Mon Sep 17 00:00:00 2001 From: ale8k Date: Mon, 26 Jun 2023 12:23:30 +0100 Subject: [PATCH 11/12] Refactor according to PR comments --- internal/jimm/model_status_parser.go | 46 +++++++---------------- internal/jimm/model_status_parser_test.go | 32 +++++----------- internal/jujuapi/jimm.go | 14 +++---- 3 files changed, 29 insertions(+), 63 deletions(-) diff --git a/internal/jimm/model_status_parser.go b/internal/jimm/model_status_parser.go index 6524bdd47..35a80f76e 100644 --- a/internal/jimm/model_status_parser.go +++ b/internal/jimm/model_status_parser.go @@ -2,7 +2,6 @@ package jimm import ( "context" - "database/sql" "encoding/json" "github.com/CanonicalLtd/jimm/api/params" @@ -25,7 +24,7 @@ import ( // If a result is erroneous, for example, bad data type parsing, the resulting struct field // Errors will contain a map from model UUID -> []error. Otherwise, the Results field // will contain model UUID -> []Jq result. -func (j *JIMM) QueryModelsJq(ctx context.Context, modelUUIDs []string, jqQuery string) (params.CrossModelQueryResponse, error) { +func (j *JIMM) QueryModelsJq(ctx context.Context, models []dbmodel.Model, jqQuery string) (params.CrossModelQueryResponse, error) { op := errors.Op("QueryModels") results := params.CrossModelQueryResponse{ Results: make(map[string][]any), @@ -41,12 +40,12 @@ func (j *JIMM) QueryModelsJq(ctx context.Context, modelUUIDs []string, jqQuery s // of each facade call and type conversion. retriever := newFormatterParamsRetriever(j) - for _, id := range modelUUIDs { - - params, err := retriever.GetParams(ctx, id) + for _, model := range models { + modelUUID := model.UUID.String + params, err := retriever.GetParams(ctx, model) if err != nil { - zapctx.Error(ctx, "failed to get status formatter params", zap.String("model-uuid", id)) - results.Errors[id] = append(results.Errors[id], err.Error()) + zapctx.Error(ctx, "failed to get status formatter params", zap.String("model-uuid", modelUUID)) + results.Errors[modelUUID] = append(results.Errors[modelUUID], err.Error()) continue } @@ -56,8 +55,8 @@ func (j *JIMM) QueryModelsJq(ctx context.Context, modelUUIDs []string, jqQuery s formattedStatus, err := formatter.Format() if err != nil { - zapctx.Error(ctx, "failed to format status", zap.String("model-uuid", id)) - results.Errors[id] = append(results.Errors[id], err.Error()) + zapctx.Error(ctx, "failed to format status", zap.String("model-uuid", modelUUID)) + results.Errors[modelUUID] = append(results.Errors[modelUUID], err.Error()) continue } // We could use output.NewFormatter() from 3.0+ juju/juju, but ultimately @@ -65,8 +64,8 @@ func (j *JIMM) QueryModelsJq(ctx context.Context, modelUUIDs []string, jqQuery s // *should* be OK. But TODO: make sure this is fine. fb, err := json.Marshal(formattedStatus) if err != nil { - zapctx.Error(ctx, "failed to marshal formatted status", zap.String("model-uuid", id)) - results.Errors[id] = append(results.Errors[id], err.Error()) + zapctx.Error(ctx, "failed to marshal formatted status", zap.String("model-uuid", modelUUID)) + results.Errors[modelUUID] = append(results.Errors[modelUUID], err.Error()) continue } tempMap := make(map[string]any) @@ -85,11 +84,11 @@ func (j *JIMM) QueryModelsJq(ctx context.Context, modelUUIDs []string, jqQuery s // query. As such, we simply append all to the errors field and continue to collect // both erreoneous and valid query results. if err, ok := v.(error); ok { - results.Errors[id] = append(results.Errors[id], "jq error: "+err.Error()) + results.Errors[modelUUID] = append(results.Errors[modelUUID], "jq error: "+err.Error()) continue } - results.Results[id] = append(results.Results[id], v) + results.Results[modelUUID] = append(results.Results[modelUUID], v) } } return results, nil @@ -118,10 +117,8 @@ func newFormatterParamsRetriever(j *JIMM) *formatterParamsRetriever { // GetParams retrieves the required parameters for the Juju status formatter from the currently // loaded model. See formatterParamsRetriever.LoadModel for more information. -func (f *formatterParamsRetriever) GetParams(ctx context.Context, modelUUID string) (*status.NewStatusFormatterParams, error) { - if err := f.loadModel(ctx, modelUUID); err != nil { - return nil, err - } +func (f *formatterParamsRetriever) GetParams(ctx context.Context, model dbmodel.Model) (*status.NewStatusFormatterParams, error) { + f.model = &model err := f.dialModel(ctx) if err != nil { @@ -148,21 +145,6 @@ func (f *formatterParamsRetriever) GetParams(ctx context.Context, modelUUID stri }, nil } -// LoadModel loads the model by UUID from the database into the formatterParamsRetriever. -// This MUST be called before attempting to GetParams(). -func (f *formatterParamsRetriever) loadModel(ctx context.Context, modelUUID string) error { - model := dbmodel.Model{ - UUID: sql.NullString{String: modelUUID, Valid: true}, - } - - if err := f.jimm.Database.GetModel(ctx, &model); err != nil { - zapctx.Error(ctx, "failed to retrieve model", zap.String("model-uuid", modelUUID)) - return err - } - f.model = &model - return nil -} - // dialModel dials the model currently loaded into the formatterParamsRetriever. func (f *formatterParamsRetriever) dialModel(ctx context.Context) error { modelTag, ok := f.model.Tag().(names.ModelTag) diff --git a/internal/jimm/model_status_parser_test.go b/internal/jimm/model_status_parser_test.go index f5518be10..f9b1448c2 100644 --- a/internal/jimm/model_status_parser_test.go +++ b/internal/jimm/model_status_parser_test.go @@ -12,6 +12,7 @@ import ( jujuparams "github.com/juju/juju/rpc/params" "github.com/CanonicalLtd/jimm/internal/db" + "github.com/CanonicalLtd/jimm/internal/dbmodel" "github.com/CanonicalLtd/jimm/internal/errors" "github.com/CanonicalLtd/jimm/internal/jimm" "github.com/CanonicalLtd/jimm/internal/jimmtest" @@ -440,14 +441,14 @@ func TestQueryModelsJq(t *testing.T) { } // Query for all models only. - userModels, err := j.Database.GetUserModels(ctx, &user) + usersModels, err := j.Database.GetUserModels(ctx, &user) c.Assert(err, qt.IsNil) - userModelUUIDs := make([]string, len(userModels)) - for i, m := range userModels { - userModelUUIDs[i] = m.Model_.UUID.String + models := make([]dbmodel.Model, len(usersModels)) + for i, m := range usersModels { + models[i] = m.Model_ } - res, err := j.QueryModelsJq(ctx, userModelUUIDs, ".model") + res, err := j.QueryModelsJq(ctx, models, ".model") c.Assert(err, qt.IsNil) c.Assert(` { @@ -507,12 +508,7 @@ func TestQueryModelsJq(t *testing.T) { `, qt.JSONEquals, res) // Query all applications across all models. - // Reset the model UUID for test (as it mutates the slice) - for i, m := range userModels { - userModelUUIDs[i] = m.Model_.UUID.String - } - - res, err = j.QueryModelsJq(ctx, userModelUUIDs, ".applications") + res, err = j.QueryModelsJq(ctx, models, ".applications") c.Assert(err, qt.IsNil) c.Assert(` { @@ -660,12 +656,7 @@ func TestQueryModelsJq(t *testing.T) { `, qt.JSONEquals, res) // Query specifically for models including the app "nginx-ingress-integrator" - // Reset the model UUID for test (as it mutates the slice) - for i, m := range userModels { - userModelUUIDs[i] = m.Model_.UUID.String - } - - res, err = j.QueryModelsJq(ctx, userModelUUIDs, ".applications | with_entries(select(.key==\"nginx-ingress-integrator\"))") + res, err = j.QueryModelsJq(ctx, models, ".applications | with_entries(select(.key==\"nginx-ingress-integrator\"))") c.Assert(err, qt.IsNil) c.Assert(` { @@ -727,12 +718,7 @@ func TestQueryModelsJq(t *testing.T) { `, qt.JSONEquals, res) // Query specifically for storage on this model. - // Reset the model UUID for test (as it mutates the slice) - for i, m := range userModels { - userModelUUIDs[i] = m.Model_.UUID.String - } - - res, err = j.QueryModelsJq(ctx, userModelUUIDs, ".storage") + res, err = j.QueryModelsJq(ctx, models, ".storage") c.Assert(err, qt.IsNil) // Not the cleanest thing in the world, but this field needs ignoring, diff --git a/internal/jujuapi/jimm.go b/internal/jujuapi/jimm.go index 8b51c11f2..0dfeab50e 100644 --- a/internal/jujuapi/jimm.go +++ b/internal/jujuapi/jimm.go @@ -451,19 +451,17 @@ func (r *controllerRoot) RemoveCloudFromController(ctx context.Context, req apip func (r *controllerRoot) CrossModelQuery(ctx context.Context, req apiparams.CrossModelQueryRequest) (apiparams.CrossModelQueryResponse, error) { const op = errors.Op("jujuapi.CrossModelQuery") - models, err := r.jimm.Database.GetUserModels(ctx, r.user) - modelUUIDs := make([]string, len(models)) - - for i, m := range models { - modelUUIDs[i] = m.Model_.UUID.String - } - + usersModels, err := r.jimm.Database.GetUserModels(ctx, r.user) if err != nil { return apiparams.CrossModelQueryResponse{}, errors.E(op, errors.Code("failed to get models for user")) } + models := make([]dbmodel.Model, len(usersModels)) + for i, m := range usersModels { + models[i] = m.Model_ + } switch strings.TrimSpace(strings.ToLower(req.Type)) { case "jq": - return r.jimm.QueryModelsJq(ctx, modelUUIDs, req.Query) + return r.jimm.QueryModelsJq(ctx, models, req.Query) case "jimmsql": return apiparams.CrossModelQueryResponse{}, errors.E(op, errors.CodeNotImplemented) default: From 6f278e9ade2201feeedacc0ca00dad85c6037ec0 Mon Sep 17 00:00:00 2001 From: ale8k Date: Mon, 26 Jun 2023 13:56:24 +0100 Subject: [PATCH 12/12] pr comments --- internal/jimm/model_status_parser.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/jimm/model_status_parser.go b/internal/jimm/model_status_parser.go index 35a80f76e..964c2906c 100644 --- a/internal/jimm/model_status_parser.go +++ b/internal/jimm/model_status_parser.go @@ -33,7 +33,7 @@ func (j *JIMM) QueryModelsJq(ctx context.Context, models []dbmodel.Model, jqQuer query, err := gojq.Parse(jqQuery) if err != nil { - return results, errors.E(op, err) + return results, errors.E(op, "failed to parse jq query", err) } // Set up a formatterParamsRetriever to handle the heavy lifting