diff --git a/cmd/grr/main.go b/cmd/grr/main.go index 4cf6c8dc..2ce4741e 100644 --- a/cmd/grr/main.go +++ b/cmd/grr/main.go @@ -20,9 +20,14 @@ func main() { Version: Version, } + gclient, err := grafana.GetClient() + if err != nil { + log.Fatalln(err) + } + grizzly.ConfigureProviderRegistry( []grizzly.Provider{ - &grafana.Provider{}, + grafana.NewProvider(gclient), }) // workflow commands diff --git a/go.mod b/go.mod index ab110700..3b4a51e3 100644 --- a/go.mod +++ b/go.mod @@ -6,8 +6,10 @@ require ( github.com/fatih/color v1.15.0 github.com/gdamore/tcell v1.4.0 github.com/go-clix/cli v0.2.0 + github.com/go-openapi/errors v0.20.4 github.com/gobwas/glob v0.2.3 github.com/google/go-jsonnet v0.20.0 + github.com/grafana/grafana-openapi-client-go v0.0.0-20231016093917-88fc2f84f257 github.com/grafana/synthetic-monitoring-agent v0.16.5 github.com/grafana/synthetic-monitoring-api-go-client v0.7.0 github.com/grafana/tanka v0.25.0 @@ -25,22 +27,40 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/semver/v3 v3.1.1 // indirect github.com/Masterminds/sprig/v3 v3.2.2 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gdamore/encoding v1.0.0 // indirect + github.com/go-logr/logr v1.2.4 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/analysis v0.21.4 // indirect + github.com/go-openapi/jsonpointer v0.20.0 // indirect + github.com/go-openapi/jsonreference v0.20.2 // indirect + github.com/go-openapi/loads v0.21.2 // indirect + github.com/go-openapi/runtime v0.26.0 // indirect + github.com/go-openapi/spec v0.20.9 // indirect + github.com/go-openapi/strfmt v0.21.7 // indirect + github.com/go-openapi/swag v0.22.4 // indirect + github.com/go-openapi/validate v0.22.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/uuid v1.3.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect + github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.12 // indirect + github.com/josharian/intern v1.0.0 // indirect github.com/lucasb-eyer/go-colorful v1.0.3 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/oklog/ulid v1.3.1 // indirect + github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/posener/complete v1.2.3 // indirect github.com/rivo/uniseg v0.1.0 // indirect @@ -49,6 +69,10 @@ require ( github.com/spf13/cast v1.4.1 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.0 // indirect + go.mongodb.org/mongo-driver v1.12.1 // indirect + go.opentelemetry.io/otel v1.17.0 // indirect + go.opentelemetry.io/otel/metric v1.17.0 // indirect + go.opentelemetry.io/otel/trace v1.17.0 // indirect golang.org/x/net v0.17.0 // indirect golang.org/x/sys v0.13.0 // indirect golang.org/x/term v0.13.0 // indirect diff --git a/go.sum b/go.sum index 4e54eccc..3c78cbb3 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= @@ -7,77 +6,92 @@ github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030I github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmyxvxX8= github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/fatih/color v1.15.0 h1:kOqh6YHBtK8aywxGerMG2Eq3H6Qgoqeo13Bk2Mv/nBs= github.com/fatih/color v1.15.0/go.mod h1:0h5ZqXfHYED7Bhv2ZJamyIOUej9KtShiJESRwBDUSsw= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= 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 v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= github.com/gdamore/tcell v1.4.0/go.mod h1:vxEiSDZdW3L+Uhjii9c3375IlDmR05bzxY404ZVSMo0= github.com/go-clix/cli v0.2.0 h1:rqpcyS/cvshOhXkwii0V+7nWetDVC8cp4pKI7JiCIS8= github.com/go-clix/cli v0.2.0/go.mod h1:yWI9abpv187r47lDjz8Z9TWev93aUTWaW2seSb5JmPQ= +github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= +github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/analysis v0.21.4 h1:ZDFLvSNxpDaomuCueM0BlSXxpANBlFYiBvr+GXrvIHc= +github.com/go-openapi/analysis v0.21.4/go.mod h1:4zQ35W4neeZTqh3ol0rv/O8JBbka9QyAgQRPp9y3pfo= +github.com/go-openapi/errors v0.20.4 h1:unTcVm6PispJsMECE3zWgvG4xTiKda1LIR5rCRWLG6M= +github.com/go-openapi/errors v0.20.4/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuAOhlsB1FSgk= +github.com/go-openapi/jsonpointer v0.20.0 h1:ESKJdU9ASRfaPNOPRx12IUyA1vn3R9GiE3KYD14BXdQ= +github.com/go-openapi/jsonpointer v0.20.0/go.mod h1:6PGzBjjIIumbLYysB73Klnms1mwnU4G3YHOECG3CedA= +github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE= +github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= +github.com/go-openapi/loads v0.21.2 h1:r2a/xFIYeZ4Qd2TnGpWDIQNcP80dIaZgf704za8enro= +github.com/go-openapi/loads v0.21.2/go.mod h1:Jq58Os6SSGz0rzh62ptiu8Z31I+OTHqmULx5e/gJbNw= +github.com/go-openapi/runtime v0.26.0 h1:HYOFtG00FM1UvqrcxbEJg/SwvDRvYLQKGhw2zaQjTcc= +github.com/go-openapi/runtime v0.26.0/go.mod h1:QgRGeZwrUcSHdeh4Ka9Glvo0ug1LC5WyE+EV88plZrQ= +github.com/go-openapi/spec v0.20.9 h1:xnlYNQAwKd2VQRRfwTEI0DcK+2cbuvI/0c7jx3gA8/8= +github.com/go-openapi/spec v0.20.9/go.mod h1:2OpW+JddWPrpXSCIX8eOx7lZ5iyuWj3RYR6VaaBKcWA= +github.com/go-openapi/strfmt v0.21.7 h1:rspiXgNWgeUzhjo1YU01do6qsahtJNByjLVbPLNHb8k= +github.com/go-openapi/strfmt v0.21.7/go.mod h1:adeGTkxE44sPyLk0JV235VQAO/ZXUr8KAzYjclFs3ew= +github.com/go-openapi/swag v0.22.4 h1:QLMzNJnMGPRNDCbySlcj1x01tzU8/9LTTL9hZZZogBU= +github.com/go-openapi/swag v0.22.4/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= +github.com/go-openapi/validate v0.22.1 h1:G+c2ub6q47kfX1sOBLwIQwzBVt8qmOAARyo/9Fqs9NU= +github.com/go-openapi/validate v0.22.1/go.mod h1:rjnrwK57VJ7A8xqfpAOEKRH8yQSGUriMu5/zuPSQ1hg= github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y= github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-jsonnet v0.20.0 h1:WG4TTSARuV7bSm4PMB4ohjxe33IHT5WVTrJSU33uT4g= github.com/google/go-jsonnet v0.20.0/go.mod h1:VbgWF9JX7ztlv770x/TolZNGGFfiHEVx9G6ca2eUmeA= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grafana/grafana-openapi-client-go v0.0.0-20231016093917-88fc2f84f257 h1:ytRI6jmcGpX3YZzfid/FPt0zZ150gS0Y/geipEAsO0w= +github.com/grafana/grafana-openapi-client-go v0.0.0-20231016093917-88fc2f84f257/go.mod h1:2vJ8YEgriYoHaNg5eijRU/q7eJTxT078VrGRSTTLeRk= github.com/grafana/synthetic-monitoring-agent v0.16.5 h1:aKspOWUQyQAH1jfzTae8+Buks+TJR3IrhTLFhWRTmG8= github.com/grafana/synthetic-monitoring-agent v0.16.5/go.mod h1:nvUY4lgjsX9MQAbsYvIhWSECcpL6i7Ml/9PXkJj3KWw= github.com/grafana/synthetic-monitoring-api-go-client v0.7.0 h1:3ZfQzmXDBPcQTTgMAIIiTw5Dwxm/B4lzf34Sto0d0YY= github.com/grafana/synthetic-monitoring-api-go-client v0.7.0/go.mod h1:ET64tbp14yq3U8X97/IO/KbG+FqU5Z8DigWrzGUHX40= github.com/grafana/tanka v0.25.0 h1:4S5ouwiXx6TDTxjLAFxhfHanJA20PfvWwMXjCOpqVQ4= github.com/grafana/tanka v0.25.0/go.mod h1:5jFLE73Qrzs/YHugOTJCCPrILbQZaIyHahrBfnXYtzA= -github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= -github.com/karrick/godirwalk v1.17.0 h1:b4kY7nqDdioR/6qnbHQyDvmA17u5G1cZ6J+CZXwSWoI= -github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= +github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 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.4/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 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= -github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= +github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= +github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -88,96 +102,48 @@ github.com/rivo/tview v0.0.0-20200818120338-53d50e499bf9 h1:csnip7QsoiE2Ee0RkELN github.com/rivo/tview v0.0.0-20200818120338-53d50e499bf9/go.mod h1:xV4Aw4WIX8cmhg71U7MUHBdpIQ7zSEXdRruGHLaEAOc= github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.29.1 h1:cO+d60CHkknCbvzEWxP0S9K6KqyTjrCNUy1LdQLCGPc= github.com/rs/zerolog v1.29.1/go.mod h1:Le6ESbR7hc+DP6Lt1THiV8CQSdkkNrd3R0XbEgp3ZBU= -github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.4.1 h1:s0hze+J0196ZfEMTs80N7UlFt0BDuQ7Q+JDnHiMWKdA= github.com/spf13/cast v1.4.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +go.mongodb.org/mongo-driver v1.12.1 h1:nLkghSU8fQNaK7oUmDhQFsnrtcoNy7Z6LVFKsEecqgE= +go.mongodb.org/mongo-driver v1.12.1/go.mod h1:/rGBTebI3XYboVmgz+Wv3Bcbl3aD0QF9zl6kDDw18rQ= +go.opentelemetry.io/otel v1.17.0 h1:MW+phZ6WZ5/uk2nd93ANk/6yJ+dVrvNWUjGhnnFU5jM= +go.opentelemetry.io/otel v1.17.0/go.mod h1:I2vmBGtFaODIVMBSTPVDlJSzBDNf93k60E6Ft0nyjo0= +go.opentelemetry.io/otel/metric v1.17.0 h1:iG6LGVz5Gh+IuO0jmgvpTB6YVrCGngi8QGm+pMd8Pdc= +go.opentelemetry.io/otel/metric v1.17.0/go.mod h1:h4skoxdZI17AxwITdmdZjjYJQH5nzijUUjm+wtPph5o= +go.opentelemetry.io/otel/trace v1.17.0 h1:/SWhSRHmDPOImIAetP1QAeMnZYiQXrTy4fMMYOdSKWQ= +go.opentelemetry.io/otel/trace v1.17.0/go.mod h1:I/4vKTgFclIsXRVucpH25X0mpFSczM7aHeaz0ZBLWjY= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200817155316-9781c653f443/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/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-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/genproto v0.0.0-20230524185152-1884fd1fac28 h1:+55/MuGJORMxCrkAgo2595fMAnN/4rweCuwibbqrvpc= google.golang.org/genproto v0.0.0-20230524185152-1884fd1fac28/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo= diff --git a/pkg/grafana/client.go b/pkg/grafana/client.go new file mode 100644 index 00000000..99eb83f7 --- /dev/null +++ b/pkg/grafana/client.go @@ -0,0 +1,31 @@ +package grafana + +import ( + "fmt" + "net/url" + "os" + + gclient "github.com/grafana/grafana-openapi-client-go/client" +) + +func GetClient() (*gclient.GrafanaHTTPAPI, error) { + grafanaURL, exists := os.LookupEnv("GRAFANA_URL") + if !exists { + return nil, fmt.Errorf("require GRAFANA_URL (optionally GRAFANA_TOKEN & GRAFANA_USER)") + } + parsedUrl, err := url.Parse(grafanaURL) + if err != nil { + return nil, fmt.Errorf("invalid Grafana URL") + } + + transportConfig := gclient.DefaultTransportConfig().WithHost(parsedUrl.Host).WithSchemes([]string{parsedUrl.Scheme}) + if token, exists := os.LookupEnv("GRAFANA_TOKEN"); exists { + if user, exists := os.LookupEnv("GRAFANA_USER"); exists { + transportConfig.BasicAuth = url.UserPassword(user, token) + } else { + transportConfig.APIKey = token + } + } + grafanaClient := gclient.NewHTTPClientWithConfig(nil, transportConfig) + return grafanaClient, nil +} diff --git a/pkg/grafana/config.go b/pkg/grafana/config.go deleted file mode 100644 index 125fb515..00000000 --- a/pkg/grafana/config.go +++ /dev/null @@ -1,68 +0,0 @@ -package grafana - -import ( - "fmt" - "net/url" - "os" - "path" - "strings" -) - -func getGrafanaToken() (string, bool) { - return os.LookupEnv("GRAFANA_TOKEN") -} - -func getGrafanaURL(urlPath string) (string, error) { - if grafanaURL, exists := os.LookupEnv("GRAFANA_URL"); exists { - u, err := url.Parse(grafanaURL) - if err != nil { - return "", err - } - parts := strings.Split(urlPath, "?") - u.Path = path.Join(u.Path, parts[0]) - if len(parts) > 1 { - u.RawQuery = parts[1] - } - if token, exists := os.LookupEnv("GRAFANA_TOKEN"); exists { - user, exists := os.LookupEnv("GRAFANA_USER") - if !exists { - user = "api_key" - } - u.User = url.UserPassword(user, token) - } - return u.String(), nil - } - return "", fmt.Errorf("Require GRAFANA_URL (optionally GRAFANA_TOKEN & GRAFANA_USER") -} - -func getWSGrafanaURL(urlPath string) (string, string, error) { - grafanaURL, exists := os.LookupEnv("GRAFANA_URL") - if !exists { - return "", "", fmt.Errorf("Require GRAFANA_URL (optionally GRAFANA_TOKEN if auth required) for websocket actions") - } - u, err := url.Parse(grafanaURL) - if err != nil { - return "", "", err - } - if u.Scheme == "https" { - u.Scheme = "wss" - } else { - u.Scheme = "ws" - } - u.Path = path.Join(u.Path, urlPath) - grafanaURL = u.String() - token, ok := os.LookupEnv("GRAFANA_TOKEN") - if ok { - u.User = nil - return u.String(), token, nil - } - if u.User != nil { - token, ok := u.User.Password() - if !ok { - return u.String(), "", nil - } - u.User = nil - return u.String(), token, nil - } - return u.String(), "", nil -} diff --git a/pkg/grafana/config_test.go b/pkg/grafana/config_test.go deleted file mode 100644 index 309eeba0..00000000 --- a/pkg/grafana/config_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package grafana - -import ( - "os" - "testing" -) - -func TestParseEnvironment(t *testing.T) { - tests := map[string]struct { - url string - path string - user string - token string - expect string - err bool - }{ - "GRAFANA_URL only": { - "https://my.grafana.net", - "/this", - "", - "", - "https://my.grafana.net/this", - false, - }, - "w/ token": { - "https://my.grafana.net", - "/that", - "", - "token", - "https://api_key:token@my.grafana.net/that", - false, - }, - "Basic auth": { - "https://my.grafana.net", - "/secure", - "user", - "pass", - "https://user:pass@my.grafana.net/secure", - false, - }, - "GRAFANA_URL blank": { - "", - "", - "", - "", - "", - true, - }, - } - for testName, test := range tests { - if test.url != "" { - os.Setenv("GRAFANA_URL", test.url) - } else { - os.Unsetenv("GRAFANA_URL") - } - if test.user != "" { - os.Setenv("GRAFANA_USER", test.user) - } else { - os.Unsetenv("GRAFANA_USER") - } - if test.token != "" { - os.Setenv("GRAFANA_TOKEN", test.token) - } else { - os.Unsetenv("GRAFANA_TOKEN") - } - t.Logf("Running test case, %q...", testName) - url, err := getGrafanaURL(test.path) - if err != nil && !test.err { - t.Errorf("Unexpected error getting Jsonnet files: %s", err) - } - if url != test.expect { - t.Errorf("Expected GrafanaURL %s, got: %s", test.expect, url) - } - } -} diff --git a/pkg/grafana/dashboard-handler.go b/pkg/grafana/dashboard-handler.go index c945cef2..7bd7547f 100644 --- a/pkg/grafana/dashboard-handler.go +++ b/pkg/grafana/dashboard-handler.go @@ -42,10 +42,6 @@ func (h *DashboardHandler) APIVersion() string { return h.Provider.APIVersion() } -const ( - dashboardFolderDefault = "General" -) - // GetExtension returns the file name extension for a dashboard func (h *DashboardHandler) GetExtension() string { return "json" @@ -70,9 +66,9 @@ func (h *DashboardHandler) ResourceFilePath(resource grizzly.Resource, filetype // Parse parses a manifest object into a struct for this resource type func (h *DashboardHandler) Parse(m manifest.Manifest) (grizzly.Resources, error) { resource := grizzly.Resource(m) - resource.SetSpecString("uid", resource.GetMetadata("name")) + resource.SetSpecString("uid", resource.Name()) if !resource.HasMetadata("folder") { - resource.SetMetadata("folder", dashboardFolderDefault) + resource.SetMetadata("folder", generalFolderUID) } return grizzly.Resources{resource}, nil } @@ -94,9 +90,9 @@ func (h *DashboardHandler) GetUID(resource grizzly.Resource) (string, error) { // GetByUID retrieves JSON for a resource from an endpoint, by UID func (h *DashboardHandler) GetByUID(UID string) (*grizzly.Resource, error) { - resource, err := getRemoteDashboard(UID) + resource, err := getRemoteDashboard(h.Provider.client, UID) if err != nil { - return nil, fmt.Errorf("Error retrieving dashboard %s: %v", UID, err) + return nil, fmt.Errorf("Error retrieving dashboard %s: %w", UID, err) } return resource, nil } @@ -107,27 +103,27 @@ func (h *DashboardHandler) GetRemote(resource grizzly.Resource) (*grizzly.Resour if uid != resource.Name() { return nil, fmt.Errorf("uid '%s' and name '%s', don't match", uid, resource.Name()) } - return getRemoteDashboard(resource.Name()) + return getRemoteDashboard(h.Provider.client, resource.Name()) } // ListRemote retrieves as list of UIDs of all remote resources func (h *DashboardHandler) ListRemote() ([]string, error) { - return getRemoteDashboardList() + return getRemoteDashboardList(h.Provider.client) } // Add pushes a new dashboard to Grafana via the API func (h *DashboardHandler) Add(resource grizzly.Resource) error { - return postDashboard(resource) + return postDashboard(h.Provider.client, resource) } // Update pushes a dashboard to Grafana via the API func (h *DashboardHandler) Update(existing, resource grizzly.Resource) error { - return postDashboard(resource) + return postDashboard(h.Provider.client, resource) } // Preview renders Jsonnet then pushes them to the endpoint if previews are possible func (h *DashboardHandler) Preview(resource grizzly.Resource, opts *grizzly.PreviewOpts) error { - s, err := postSnapshot(resource, opts) + s, err := postSnapshot(h.Provider.client, resource, opts) if err != nil { return err } diff --git a/pkg/grafana/dashboards.go b/pkg/grafana/dashboards.go index edcfd89c..b130c88b 100644 --- a/pkg/grafana/dashboards.go +++ b/pkg/grafana/dashboards.go @@ -1,130 +1,87 @@ package grafana import ( - "bytes" - "encoding/json" "errors" "fmt" - "io" - "net/http" + + gclient "github.com/grafana/grafana-openapi-client-go/client" + "github.com/grafana/grafana-openapi-client-go/client/dashboards" + "github.com/grafana/grafana-openapi-client-go/client/search" + "github.com/grafana/grafana-openapi-client-go/client/snapshots" + "github.com/grafana/grafana-openapi-client-go/models" "github.com/grafana/grizzly/pkg/grizzly" ) -// getRemoteDashboard retrieves a dashboard object from Grafana -func getRemoteDashboard(uid string) (*grizzly.Resource, error) { - client := new(http.Client) - grafanaURL, err := getGrafanaURL("api/dashboards/uid/" + uid) - if err != nil { - return nil, err - } +// Moved from utils.go +const generalFolderId = 0 +const generalFolderUID = "general" - req, err := http.NewRequest("GET", grafanaURL, nil) - if err != nil { - return nil, err - } +// Losing id, typeLogoUrl, version, withCredentials - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } - resp, err := client.Do(req) +// getRemoteDashboard retrieves a dashboard object from Grafana +func getRemoteDashboard(client *gclient.GrafanaHTTPAPI, uid string) (*grizzly.Resource, error) { + params := dashboards.NewGetDashboardByUIDParams().WithUID(uid) + dashboardOk, err := client.Dashboards.GetDashboardByUID(params, nil) if err != nil { + var gErr *dashboards.GetDashboardByUIDNotFound + if errors.As(err, &gErr) { + return nil, grizzly.ErrNotFound + } return nil, err } - defer resp.Body.Close() + dashboard := dashboardOk.GetPayload() - switch { - case resp.StatusCode == http.StatusNotFound: - return nil, grizzly.ErrNotFound - case resp.StatusCode >= 400: - return nil, errors.New(resp.Status) - } - - data, err := io.ReadAll(resp.Body) + // TODO: Turn spec into a real models.DashboardFullWithMeta object + spec, err := structToMap(dashboard.Dashboard) if err != nil { return nil, err } - var d DashboardWrapper - if err := json.Unmarshal(data, &d); err != nil { - return nil, grizzly.APIErr{Err: err, Body: data} - } - delete(d.Dashboard, "id") - delete(d.Dashboard, "version") h := DashboardHandler{} - resource := grizzly.NewResource(h.APIVersion(), h.Kind(), uid, d.Dashboard) - folderUid := extractFolderUID(d) + resource := grizzly.NewResource(h.APIVersion(), h.Kind(), uid, spec) + folderUid := extractFolderUID(client, *dashboard) resource.SetMetadata("folder", folderUid) return &resource, nil } -func getRemoteDashboardList() ([]string, error) { - batchSize := 500 - - client := new(http.Client) - UIDs := []string{} - for page := 1; ; page++ { - grafanaURL, err := getGrafanaURL(fmt.Sprintf("/api/search?type=dash-db&limit=%d&page=%d", batchSize, page)) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", grafanaURL, nil) - if err != nil { - return nil, err - } +func getRemoteDashboardList(client *gclient.GrafanaHTTPAPI) ([]string, error) { + var ( + limit = int64(1000) + searchType = "dash-db" + page int64 = 0 + uids []string + ) - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } + params := search.NewSearchParams().WithLimit(&limit).WithType(&searchType) + for { + page++ + params.SetPage(&page) - resp, err := client.Do(req) + searchOk, err := client.Search.Search(params, nil) if err != nil { return nil, err } - defer resp.Body.Close() - switch { - case resp.StatusCode == http.StatusNotFound: - return nil, grizzly.ErrNotFound - case resp.StatusCode >= 400: - return nil, errors.New(resp.Status) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - var dashboards []Dashboard - if err := json.Unmarshal([]byte(string(body)), &dashboards); err != nil { - return nil, grizzly.APIErr{Err: err, Body: body} + for _, hit := range searchOk.GetPayload() { + uids = append(uids, hit.UID) } - for _, dashboard := range dashboards { - UIDs = append(UIDs, dashboard.UID()) - } - if len(dashboards) < batchSize { - break + if int64(len(searchOk.GetPayload())) < *params.Limit { + return uids, nil } } - return UIDs, nil - } -func postDashboard(resource grizzly.Resource) error { - client := new(http.Client) - grafanaURL, err := getGrafanaURL("api/dashboards/db") - if err != nil { - return err - } - +func postDashboard(client *gclient.GrafanaHTTPAPI, resource grizzly.Resource) error { folderUID := resource.GetMetadata("folder") var folderID int64 if !(folderUID == "General" || folderUID == "general") { - folder, err := getRemoteFolder(folderUID) + folder, err := getRemoteFolder(client, folderUID) if err != nil { if errors.Is(err, grizzly.ErrNotFound) { - return fmt.Errorf("Cannot upload dashboard %s as folder %s not found", resource.GetMetadata("name"), folderUID) + return fmt.Errorf("cannot upload dashboard %s as folder %s not found", resource.Name(), folderUID) } else { - return fmt.Errorf("Cannot upload dashboard %s: %w", resource.GetMetadata("name"), err) + return fmt.Errorf("cannot upload dashboard %s: %w", resource.Name(), err) } } folderID = int64(folder.GetSpecValue("id").(float64)) @@ -132,165 +89,27 @@ func postDashboard(resource grizzly.Resource) error { folderID = generalFolderId } - wrappedBoard := DashboardWrapper{ - Dashboard: resource["spec"].(map[string]interface{}), + body := models.SaveDashboardCommand{ + Dashboard: resource.Spec(), FolderID: folderID, Overwrite: true, } - wrappedJSON, err := wrappedBoard.toJSON() - - req, err := http.NewRequest("POST", grafanaURL, bytes.NewBufferString(wrappedJSON)) - if err != nil { - return err - } - - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := client.Do(req) - if err != nil { - return err - } - - switch resp.StatusCode { - case http.StatusOK: - return nil - case http.StatusPreconditionFailed: - d := json.NewDecoder(resp.Body) - var r struct { - Message string `json:"message"` - } - if err := d.Decode(&r); err != nil { - return fmt.Errorf("Failed to decode actual error (412 Precondition failed): %s", err) - } - fmt.Println(wrappedJSON) - return fmt.Errorf("Error while applying '%s' to Grafana: %s", resource.Name(), r.Message) - default: - return NewErrNon200Response("dashboard", resource.Name(), resp) - } + params := dashboards.NewPostDashboardParams().WithBody(&body) + _, err := client.Dashboards.PostDashboard(params, nil) + return err } -// SnapshotResp encapsulates the response to a snapshot request -type SnapshotResp struct { - DeleteKey string `json:"deleteKey"` - DeleteURL string `json:"deleteUrl"` - Key string `json:"key"` - URL string `json:"url"` -} - -func postSnapshot(resource grizzly.Resource, opts *grizzly.PreviewOpts) (*SnapshotResp, error) { - client := new(http.Client) - url, err := getGrafanaURL("api/snapshots") - if err != nil { - return nil, err - } - type SnapshotReq struct { - Dashboard map[string]interface{} `json:"dashboard"` - Expires int `json:"expires,omitempty"` - } - - sr := &SnapshotReq{ - Dashboard: resource["spec"].(map[string]interface{}), +func postSnapshot(client *gclient.GrafanaHTTPAPI, resource grizzly.Resource, opts *grizzly.PreviewOpts) (*models.CreateDashboardSnapshotOKBody, error) { + body := models.CreateDashboardSnapshotCommand{ + Dashboard: resource.Spec(), } - if opts.ExpiresSeconds > 0 { - sr.Expires = opts.ExpiresSeconds + body.Expires = int64(opts.ExpiresSeconds) } - - bs, err := json.Marshal(&sr) + params := snapshots.NewCreateDashboardSnapshotParams().WithBody(&body) + response, err := client.Snapshots.CreateDashboardSnapshot(params, nil) if err != nil { return nil, err } - - req, err := http.NewRequest("POST", url, bytes.NewBuffer(bs)) - if err != nil { - return nil, err - } - - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - if resp.StatusCode != 200 { - return nil, NewErrNon200Response("snapshot", resource.Name(), resp) - - } - - b, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("Unable to read response body: %w", err) - } - - s := &SnapshotResp{} - err = json.Unmarshal(b, s) - if err != nil { - return nil, fmt.Errorf("Unable to unmarshal response body into SnapshotResp: %w", err) - } - return s, nil -} - -// Dashboard encapsulates a dashboard -type Dashboard map[string]interface{} - -// UID retrieves the UID from a dashboard -func (d *Dashboard) UID() string { - uid, ok := (*d)["uid"] - if !ok { - return "" - } - return uid.(string) -} - -// toJSON returns JSON for a dashboard -func (d *Dashboard) toJSON() (string, error) { - j, err := json.MarshalIndent(d, "", " ") - if err != nil { - return "", err - } - return string(j), nil -} - -// folderUID retrieves the folder UID for a dashboard -func (d *Dashboard) folderUID() string { - folderUID, ok := (*d)["folderName"] - if ok { - return folderUID.(string) - } - return "" -} - -// DashboardWrapper adds wrapper to a dashboard JSON. Caters both for Grafana's POST -// API as well as GET which require different JSON. -type DashboardWrapper struct { - Dashboard Dashboard `json:"dashboard"` - FolderID int64 `json:"folderId"` - Overwrite bool `json:"overwrite"` - Meta struct { - FolderID int64 `json:"folderId"` - FolderTitle string `json:"folderTitle"` - FolderUID string `json:"folderUid"` - FolderURL string `json:"folderUrl"` - } `json:"meta"` -} - -// UID retrieves the UID from a dashboard wrapper -func (d *DashboardWrapper) UID() string { - return d.Dashboard.UID() -} - -// toJSON returns JSON expected by Grafana API -func (d *DashboardWrapper) toJSON() (string, error) { - d.Overwrite = true - j, err := json.MarshalIndent(d, "", " ") - if err != nil { - return "", err - } - return string(j), nil + return response.GetPayload(), nil } diff --git a/pkg/grafana/dashboards_test.go b/pkg/grafana/dashboards_test.go index 9db32f9f..1cca21ec 100644 --- a/pkg/grafana/dashboards_test.go +++ b/pkg/grafana/dashboards_test.go @@ -13,30 +13,36 @@ import ( func TestDashboard(t *testing.T) { os.Setenv("GRAFANA_URL", GetUrl()) + grafanaClient, err := GetClient() + require.NoError(t, err) + grizzly.ConfigureProviderRegistry( []grizzly.Provider{ - &Provider{}, + NewProvider(grafanaClient), }) ticker := PingService(GetUrl()) defer ticker.Stop() + handler, err := grizzly.Registry.GetHandler((&DashboardHandler{}).Kind()) + require.NoError(t, err) + t.Run("get remote dashboard - success", func(t *testing.T) { - resource, err := getRemoteDashboard("ReciqtgGk") + resource, err := handler.GetByUID("ReciqtgGk") require.NoError(t, err) - require.Equal(t, resource.APIVersion(), "grizzly.grafana.com/v1alpha1") - require.Equal(t, resource.Name(), "ReciqtgGk") + require.Equal(t, "grizzly.grafana.com/v1alpha1", resource.APIVersion()) + require.Equal(t, "ReciqtgGk", resource.Name()) require.NotEmpty(t, resource.GetMetadata("folder")) }) t.Run("get remote dashboard - not found", func(t *testing.T) { - _, err := getRemoteDashboard("dummy") - require.EqualError(t, err, "not found") + _, err := handler.GetByUID("dummy") + require.ErrorContains(t, err, "not found") }) t.Run("get remote dashboard list - success", func(t *testing.T) { - list, err := getRemoteDashboardList() + list, err := handler.ListRemote() require.NoError(t, err) require.Len(t, list, 3) @@ -52,27 +58,26 @@ func TestDashboard(t *testing.T) { err = json.Unmarshal(dashboard, &resource) require.NoError(t, err) - err = postDashboard(resource) + err = handler.Add(resource) require.NoError(t, err) - dash, err := getRemoteDashboard("d4sHb0ard-") + dash, err := handler.GetByUID("d4sHb0ard-") require.NoError(t, err) require.NotNil(t, dash) - require.Equal(t, resource.GetMetadata("folder"), "abcdefghi") + require.Equal(t, "abcdefghi", resource.GetMetadata("folder")) }) t.Run("post remote dashboard - not found", func(t *testing.T) { - var resource grizzly.Resource - resource = map[string]interface{}{ + resource := map[string]interface{}{ "metadata": map[string]interface{}{ "folder": "dummy", "name": "dummy", }, } - err := postDashboard(resource) - require.EqualError(t, err, "Cannot upload dashboard dummy as folder dummy not found") + err := handler.Add(resource) + require.EqualError(t, err, "cannot upload dashboard dummy as folder dummy not found") }) t.Run("Check getUID is functioning correctly", func(t *testing.T) { @@ -84,7 +89,7 @@ func TestDashboard(t *testing.T) { handler := DashboardHandler{} uid, err := handler.GetUID(resource) require.NoError(t, err) - require.Equal(t, uid, "test") + require.Equal(t, "test", uid) }) _ = os.Unsetenv("GRAFANA_URL") diff --git a/pkg/grafana/datasource-handler.go b/pkg/grafana/datasource-handler.go index 99691e81..3ac38945 100644 --- a/pkg/grafana/datasource-handler.go +++ b/pkg/grafana/datasource-handler.go @@ -78,7 +78,7 @@ func (h *DatasourceHandler) Parse(m manifest.Manifest) (grizzly.Resources, error "withCredentials": false, "readOnly": false, } - spec := resource["spec"].(map[string]interface{}) + spec := resource.Spec() for k := range defaults { _, ok := spec[k] if !ok { @@ -111,25 +111,25 @@ func (h *DatasourceHandler) GetUID(resource grizzly.Resource) (string, error) { // GetByUID retrieves JSON for a resource from an endpoint, by UID func (h *DatasourceHandler) GetByUID(UID string) (*grizzly.Resource, error) { - return getRemoteDatasource(UID) + return getRemoteDatasource(h.Provider.client, UID) } // GetRemote retrieves a datasource as a Resource func (h *DatasourceHandler) GetRemote(resource grizzly.Resource) (*grizzly.Resource, error) { - return getRemoteDatasource(resource.Name()) + return getRemoteDatasource(h.Provider.client, resource.Name()) } // ListRemote retrieves as list of UIDs of all remote resources func (h *DatasourceHandler) ListRemote() ([]string, error) { - return getRemoteDatasourceList() + return getRemoteDatasourceList(h.Provider.client) } // Add pushes a datasource to Grafana via the API func (h *DatasourceHandler) Add(resource grizzly.Resource) error { - return postDatasource(resource) + return postDatasource(h.Provider.client, resource) } // Update pushes a datasource to Grafana via the API func (h *DatasourceHandler) Update(existing, resource grizzly.Resource) error { - return putDatasource(resource) + return putDatasource(h.Provider.client, resource) } diff --git a/pkg/grafana/datasource_test.go b/pkg/grafana/datasource_test.go index 9a29add7..7d799792 100644 --- a/pkg/grafana/datasource_test.go +++ b/pkg/grafana/datasource_test.go @@ -13,30 +13,36 @@ import ( func TestDatasources(t *testing.T) { os.Setenv("GRAFANA_URL", GetUrl()) + grafanaClient, err := GetClient() + require.NoError(t, err) + grizzly.ConfigureProviderRegistry( []grizzly.Provider{ - &Provider{}, + NewProvider(grafanaClient), }) ticker := PingService(GetUrl()) defer ticker.Stop() + handler, err := grizzly.Registry.GetHandler((&DatasourceHandler{}).Kind()) + require.NoError(t, err) + t.Run("get remote datasource - success", func(t *testing.T) { - resource, err := getRemoteDatasource("AppDynamics") + resource, err := handler.GetByUID("AppDynamics") require.NoError(t, err) - require.Equal(t, resource.APIVersion(), "grizzly.grafana.com/v1alpha1") - require.Equal(t, resource.Name(), "AppDynamics") - require.Len(t, resource.Spec(), 18) + require.Equal(t, "grizzly.grafana.com/v1alpha1", resource.APIVersion()) + require.Equal(t, "AppDynamics", resource.Name()) + require.Len(t, resource.Spec(), 12) }) t.Run("get remote datasource - not found", func(t *testing.T) { - _, err := getRemoteDatasource("dummy") - require.Equal(t, err, grizzly.ErrNotFound) + _, err := handler.GetByUID("dummy") + require.Equal(t, grizzly.ErrNotFound, err) }) t.Run("get remote datasources list", func(t *testing.T) { - resources, err := getRemoteDatasourceList() + resources, err := handler.ListRemote() require.NoError(t, err) require.NotNil(t, resources) @@ -52,24 +58,24 @@ func TestDatasources(t *testing.T) { err = json.Unmarshal(datasource, &resource) require.NoError(t, err) - err = postDatasource(resource) + err = handler.Add(resource) require.NoError(t, err) - ds, err := getRemoteDatasource("appdynamics") + ds, err := handler.GetByUID("appdynamics") require.NoError(t, err) require.NotNil(t, ds) - require.Equal(t, ds.Spec()["type"], "dlopes7-appdynamics-datasource") + require.Equal(t, "dlopes7-appdynamics-datasource", ds.Spec()["type"]) t.Run("put remote datasource - update", func(t *testing.T) { ds.SetSpecString("type", "new-type") - err := putDatasource(*ds) + err := handler.Add(*ds) require.NoError(t, err) - updatedDS, err := getRemoteDatasource("appdynamics") + updatedDS, err := handler.GetByUID("appdynamics") require.NoError(t, err) - require.Equal(t, updatedDS.Spec()["type"], "new-type") + require.Equal(t, "new-type", updatedDS.Spec()["type"]) }) }) @@ -84,11 +90,11 @@ func TestDatasources(t *testing.T) { resource.SetSpecString("name", "AppDynamics") - err = postDatasource(resource) + err = handler.Add(resource) - grafanaErr := err.(ErrNon200Response) - require.Error(t, err) - require.Equal(t, grafanaErr.Response.StatusCode, 409) + var non200ResponseErr ErrNon200Response + require.ErrorAs(t, err, &non200ResponseErr) + require.Equal(t, 409, non200ResponseErr.Response.StatusCode) }) t.Run("Check getUID is functioning correctly", func(t *testing.T) { @@ -100,6 +106,6 @@ func TestDatasources(t *testing.T) { handler := DatasourceHandler{} uid, err := handler.GetUID(resource) require.NoError(t, err) - require.Equal(t, uid, "test") + require.Equal(t, "test", uid) }) } diff --git a/pkg/grafana/datasources.go b/pkg/grafana/datasources.go index ff54ecf2..b3deadb3 100644 --- a/pkg/grafana/datasources.go +++ b/pkg/grafana/datasources.go @@ -1,205 +1,110 @@ package grafana import ( - "bytes" "encoding/json" "errors" - "fmt" - "io" "net/http" + "strconv" + "github.com/go-openapi/runtime" + gclient "github.com/grafana/grafana-openapi-client-go/client" + "github.com/grafana/grafana-openapi-client-go/client/datasources" + "github.com/grafana/grafana-openapi-client-go/models" "github.com/grafana/grizzly/pkg/grizzly" ) -func makeDatasourceRequest(url string) ([]byte, error) { - client := new(http.Client) - grafanaURL, err := getGrafanaURL(url) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", grafanaURL, nil) - if err != nil { - return nil, err - } - - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - switch { - case resp.StatusCode == http.StatusNotFound: - return nil, grizzly.ErrNotFound - case resp.StatusCode >= 400: - return nil, errors.New(resp.Status) - } - - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - return data, nil -} +// Losing a bunch of omitempty fields // getRemoteDatasource retrieves a datasource object from Grafana -func getRemoteDatasource(uid string) (*grizzly.Resource, error) { - data, err := makeDatasourceRequest("api/datasources/uid/" + uid) - if errors.Is(err, grizzly.ErrNotFound) { - data, err = makeDatasourceRequest("api/datasources/name/" + uid) +func getRemoteDatasource(client *gclient.GrafanaHTTPAPI, uid string) (*grizzly.Resource, error) { + h := DatasourceHandler{} + + params := datasources.NewGetDataSourceByUIDParams().WithUID(uid) + datasourceOk, err := client.Datasources.GetDataSourceByUID(params, nil) + var datasource *models.DataSource + if err != nil { + var gErr *datasources.GetDataSourceByUIDNotFound + if errors.As(err, &gErr) { + params := datasources.NewGetDataSourceByNameParams().WithName(uid) + datasourceOk, err := client.Datasources.GetDataSourceByName(params, nil) + if err != nil { + // OpenAPI definition does not define 404 for GetDataSourceByName, so falls though to runtime.APIError. + var gErr *runtime.APIError + if errors.As(err, &gErr) && gErr.IsCode(http.StatusNotFound) { + return nil, grizzly.ErrNotFound + } + return nil, err + } else { + datasource = datasourceOk.GetPayload() + } + } else { + return nil, err + } + } else { + datasource = datasourceOk.GetPayload() } + + // TODO: Turn spec into a real models.Datasource object + spec, err := structToMap(datasource) if err != nil { return nil, err } - var d map[string]interface{} - if err := json.Unmarshal(data, &d); err != nil { - return nil, grizzly.APIErr{Err: err, Body: data} - } - handler := DatasourceHandler{} - resource := grizzly.NewResource(handler.APIVersion(), handler.Kind(), uid, d) + resource := grizzly.NewResource(h.APIVersion(), h.Kind(), uid, spec) return &resource, nil } -func getRemoteDatasourceList() ([]string, error) { - client := new(http.Client) - grafanaURL, err := getGrafanaURL("api/datasources") - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", grafanaURL, nil) - if err != nil { - return nil, err - } - - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } - - resp, err := client.Do(req) +func getRemoteDatasourceList(client *gclient.GrafanaHTTPAPI) ([]string, error) { + params := datasources.NewGetDataSourcesParams() + datasourcesOk, err := client.Datasources.GetDataSources(params, nil) if err != nil { return nil, err } - defer resp.Body.Close() - - switch { - case resp.StatusCode == http.StatusNotFound: - return nil, grizzly.ErrNotFound - case resp.StatusCode >= 400: - return nil, errors.New(resp.Status) - } + datasources := datasourcesOk.GetPayload() - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - var datasources []map[string]interface{} - if err := json.Unmarshal([]byte(string(body)), &datasources); err != nil { - return nil, grizzly.APIErr{Err: err, Body: body} + uids := make([]string, len(datasources)) + for i, datasource := range datasources { + uids[i] = datasource.UID } - UIDs := []string{} - for _, datasource := range datasources { - UID := datasource["uid"].(string) - UIDs = append(UIDs, UID) - } - return UIDs, nil + return uids, nil } -func postDatasource(resource grizzly.Resource) error { - client := new(http.Client) - grafanaURL, err := getGrafanaURL("api/datasources") - if err != nil { - return err - } - - sourceJSON, err := resource.SpecAsJSON() +func postDatasource(client *gclient.GrafanaHTTPAPI, resource grizzly.Resource) error { + // TODO: Turn spec into a real models.DataSource object + data, err := json.Marshal(resource.Spec()) if err != nil { return err } - req, err := http.NewRequest("POST", grafanaURL, bytes.NewBufferString(sourceJSON)) + var datasource models.AddDataSourceCommand + err = json.Unmarshal(data, &datasource) if err != nil { return err } - - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := client.Do(req) - if err != nil { - return err - } - - switch resp.StatusCode { - case http.StatusOK: - break - case http.StatusPreconditionFailed: - d := json.NewDecoder(resp.Body) - var r struct { - Message string `json:"message"` - } - if err := d.Decode(&r); err != nil { - return fmt.Errorf("Failed to decode actual error (412 Precondition failed): %s", err) - } - fmt.Println(sourceJSON) - return fmt.Errorf("Error while applying '%s' to Grafana: %s", resource.Name(), r.Message) - default: - return NewErrNon200Response("datasource", resource.Name(), resp) - } - return nil + params := datasources.NewAddDataSourceParams().WithBody(&datasource) + _, err = client.Datasources.AddDataSource(params, nil) + return err } -func putDatasource(resource grizzly.Resource) error { - spec := resource.Spec() - id := int64(spec["id"].(float64)) - grafanaURL, err := getGrafanaURL(fmt.Sprintf("api/datasources/%d", id)) +func putDatasource(client *gclient.GrafanaHTTPAPI, resource grizzly.Resource) error { + // TODO: Turn spec into a real models.DataSource object + data, err := json.Marshal(resource.Spec()) if err != nil { return err } - sourceJSON, err := resource.SpecAsJSON() + var modelDatasource models.DataSource + err = json.Unmarshal(data, &modelDatasource) if err != nil { return err } - client, err := NewHttpClient() + var datasource models.UpdateDataSourceCommand + err = json.Unmarshal(data, &datasource) if err != nil { return err } - req, err := http.NewRequest("PUT", grafanaURL, bytes.NewBufferString(sourceJSON)) - req.Header.Add("Content-type", "application/json") - - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } - - resp, err := client.Do(req) - if err != nil { - return err - } - - switch resp.StatusCode { - case http.StatusOK: - break - case http.StatusPreconditionFailed: - d := json.NewDecoder(resp.Body) - var r struct { - Message string `json:"message"` - } - if err := d.Decode(&r); err != nil { - return fmt.Errorf("Failed to decode actual error (412 Precondition failed): %s", err) - } - fmt.Println(sourceJSON) - return fmt.Errorf("Error while applying '%s' to Grafana: %s", resource.Name(), r.Message) - default: - return NewErrNon200Response("datasource", resource.Name(), resp) - } - return nil + params := datasources.NewUpdateDataSourceByIDParams().WithID(strconv.FormatInt(modelDatasource.ID, 10)).WithBody(&datasource) + _, err = client.Datasources.UpdateDataSourceByID(params, nil) + return err } diff --git a/pkg/grafana/folder-handler.go b/pkg/grafana/folder-handler.go index 5631b33b..941440fe 100644 --- a/pkg/grafana/folder-handler.go +++ b/pkg/grafana/folder-handler.go @@ -66,7 +66,7 @@ func (h *FolderHandler) ResourceFilePath(resource grizzly.Resource, filetype str // Parse parses a manifest object into a struct for this resource type func (h *FolderHandler) Parse(m manifest.Manifest) (grizzly.Resources, error) { resource := grizzly.Resource(m) - resource.SetSpecString("uid", resource.GetMetadata("name")) + resource.SetSpecString("uid", resource.Name()) return grizzly.Resources{resource}, nil } @@ -87,9 +87,9 @@ func (h *FolderHandler) GetUID(resource grizzly.Resource) (string, error) { // GetByUID retrieves JSON for a resource from an endpoint, by UID func (h *FolderHandler) GetByUID(UID string) (*grizzly.Resource, error) { - resource, err := getRemoteFolder(UID) + resource, err := getRemoteFolder(h.Provider.client, UID) if err != nil { - return nil, fmt.Errorf("Error retrieving dashboard folder %s: %v", UID, err) + return nil, fmt.Errorf("Error retrieving dashboard folder %s: %w", UID, err) } return resource, nil @@ -97,20 +97,20 @@ func (h *FolderHandler) GetByUID(UID string) (*grizzly.Resource, error) { // GetRemote retrieves a folder as a resource func (h *FolderHandler) GetRemote(resource grizzly.Resource) (*grizzly.Resource, error) { - return getRemoteFolder(resource.Name()) + return getRemoteFolder(h.Provider.client, resource.Name()) } // ListRemote retrieves as list of UIDs of all remote resources func (h *FolderHandler) ListRemote() ([]string, error) { - return getRemoteFolderList() + return getRemoteFolderList(h.Provider.client) } // Add pushes a new folder to Grafana via the API func (h *FolderHandler) Add(resource grizzly.Resource) error { - return postFolder(resource) + return postFolder(h.Provider.client, resource) } // Update pushes a folder to Grafana via the API func (h *FolderHandler) Update(existing, resource grizzly.Resource) error { - return putFolder(resource) + return putFolder(h.Provider.client, resource) } diff --git a/pkg/grafana/folders.go b/pkg/grafana/folders.go index 5038c479..5ecc60c5 100644 --- a/pkg/grafana/folders.go +++ b/pkg/grafana/folders.go @@ -1,272 +1,135 @@ package grafana import ( - "bytes" "encoding/json" "errors" "fmt" - "io" - "net/http" + gclient "github.com/grafana/grafana-openapi-client-go/client" + "github.com/grafana/grafana-openapi-client-go/client/folders" + "github.com/grafana/grafana-openapi-client-go/models" "github.com/grafana/grizzly/pkg/grizzly" ) // getRemoteFolder retrieves a folder object from Grafana -func getRemoteFolder(uid string) (*grizzly.Resource, error) { - client := new(http.Client) +func getRemoteFolder(client *gclient.GrafanaHTTPAPI, uid string) (*grizzly.Resource, error) { h := FolderHandler{} + var folder *models.Folder if uid == "General" || uid == "general" { - folder := Folder{ - "id": 0.0, - "uid": uid, - "title": "General", + folder = &models.Folder{ + ID: 0, + UID: uid, + Title: "General", + // URL: ?? } - resource := grizzly.NewResource(h.APIVersion(), h.Kind(), uid, folder) - return &resource, nil - } - grafanaURL, err := getGrafanaURL("api/folders/" + uid) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", grafanaURL, nil) - if err != nil { - return nil, err - } - - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - switch { - case resp.StatusCode == http.StatusNotFound: - return nil, fmt.Errorf("couldn't fetch folder '%s' from remote: %w", uid, grizzly.ErrNotFound) - case resp.StatusCode >= 400: - return nil, errors.New(resp.Status) + } else { + params := folders.NewGetFolderByUIDParams().WithFolderUID(uid) + folderOk, err := client.Folders.GetFolderByUID(params, nil) + if err != nil { + var gErr *folders.GetFolderByUIDNotFound + if errors.As(err, &gErr) { + return nil, fmt.Errorf("couldn't fetch folder '%s' from remote: %w", uid, grizzly.ErrNotFound) + } + return nil, err + } + folder = folderOk.GetPayload() } - data, err := io.ReadAll(resp.Body) + // TODO: Turn spec into a real models.Folder object + spec, err := structToMap(folder) if err != nil { return nil, err } - var f Folder - if err := json.Unmarshal(data, &f); err != nil { - return nil, grizzly.APIErr{Err: err, Body: data} - } - resource := grizzly.NewResource(h.APIVersion(), h.Kind(), uid, f) + resource := grizzly.NewResource(h.APIVersion(), h.Kind(), uid, spec) return &resource, nil } -func getRemoteFolderList() ([]string, error) { - batchSize := 100 - - client := new(http.Client) - - UIDs := []string{} - for page := 1; ; page++ { - grafanaURL, err := getGrafanaURL(fmt.Sprintf("/api/search?type=dash-folder&limit=%d&page=%d", batchSize, page)) +func getRemoteFolderList(client *gclient.GrafanaHTTPAPI) ([]string, error) { + var ( + limit = int64(1000) + page int64 = 0 + uids []string + ) + params := folders.NewGetFoldersParams().WithLimit(&limit) + for { + page++ + params.SetPage(&page) + + foldersOk, err := client.Folders.GetFolders(params, nil) if err != nil { return nil, err } - req, err := http.NewRequest("GET", grafanaURL, nil) - if err != nil { - return nil, err + for _, folder := range foldersOk.GetPayload() { + uids = append(uids, folder.UID) } - - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - switch { - case resp.StatusCode == http.StatusNotFound: - return nil, fmt.Errorf("couldn't fetch folder list from remote: %w", grizzly.ErrNotFound) - case resp.StatusCode >= 400: - return nil, errors.New(resp.Status) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - var folders []Folder - if err := json.Unmarshal([]byte(string(body)), &folders); err != nil { - return nil, grizzly.APIErr{Err: err, Body: body} - } - for _, folder := range folders { - UIDs = append(UIDs, folder.UID()) - } - if len(folders) < batchSize { - break + if int64(len(foldersOk.GetPayload())) < *params.Limit { + return uids, nil } } - return UIDs, nil - } -func postFolder(resource grizzly.Resource) error { - name := resource.GetMetadata("name") - client := new(http.Client) +func postFolder(client *gclient.GrafanaHTTPAPI, resource grizzly.Resource) error { + name := resource.Name() if name == "General" || name == "general" { return nil } - grafanaURL, err := getGrafanaURL("api/folders") - if err != nil { - return err - } - folder := Folder(resource["spec"].(map[string]interface{})) - folder["uid"] = resource.GetMetadata("name") - folderJSON, err := folder.toJSON() - - req, err := http.NewRequest("POST", grafanaURL, bytes.NewBufferString(folderJSON)) + // TODO: Turn spec into a real models.Folder object + data, err := json.Marshal(resource.Spec()) if err != nil { return err } - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } - req.Header.Set("Content-Type", "application/json") - - resp, err := client.Do(req) + var folder models.Folder + err = json.Unmarshal(data, &folder) if err != nil { return err } + if folder.Title == "" { + return fmt.Errorf("missing title in folder spec") + } - switch resp.StatusCode { - case http.StatusOK: - break - case http.StatusPreconditionFailed: - d := json.NewDecoder(resp.Body) - var r struct { - Message string `json:"message"` - } - if err := d.Decode(&r); err != nil { - return fmt.Errorf("Failed to decode actual error (412 Precondition failed): %s", err) - } - return fmt.Errorf("Error while applying '%s' to Grafana: %s", resource.Name(), r.Message) - default: - return NewErrNon200Response("folder", resource.Name(), resp) + body := models.CreateFolderCommand{ + Title: folder.Title, + UID: folder.UID, } - return nil + params := folders.NewCreateFolderParams().WithBody(&body) + _, err = client.Folders.CreateFolder(params, nil) + return err } -func putFolder(resource grizzly.Resource) error { - uid := resource.GetMetadata("name") - grafanaURL, err := getGrafanaURL("api/folders/" + uid) +func putFolder(client *gclient.GrafanaHTTPAPI, resource grizzly.Resource) error { + // TODO: Turn spec into a real models.Folder object + data, err := json.Marshal(resource.Spec()) if err != nil { return err } - folder := Folder(resource["spec"].(map[string]interface{})) - folder["overwrite"] = true - folderJSON, err := folder.toJSON() - req, err := http.NewRequest(http.MethodPut, grafanaURL, bytes.NewBufferString(folderJSON)) + var folder models.Folder + err = json.Unmarshal(data, &folder) if err != nil { return err } - req.Header.Set("Content-Type", "application/json") - - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) + if folder.Title == "" { + return fmt.Errorf("missing title in folder spec") } - client := &http.Client{} - resp, err := client.Do(req) - if err != nil { - return nil - } - - switch resp.StatusCode { - case http.StatusOK: - break - case http.StatusPreconditionFailed: - d := json.NewDecoder(resp.Body) - var r struct { - Message string `json:"message"` - } - if err := d.Decode(&r); err != nil { - return fmt.Errorf("Failed to decode actual error (412 Precondition failed): %s", err) - } - return fmt.Errorf("Error while applying '%s' to Grafana: %s", resource.Name(), r.Message) - default: - return NewErrNon200Response("folder", resource.Name(), resp) + body := models.UpdateFolderCommand{ + Title: folder.Title, + UID: folder.UID, } - - return nil + params := folders.NewUpdateFolderParams().WithBody(&body) + _, err = client.Folders.UpdateFolder(params, nil) + return err } -var getFolderById = func(folderId int64) (Folder, error) { - client := new(http.Client) - grafanaURL, err := getGrafanaURL(fmt.Sprintf("folders/id/%d", folderId)) - if err != nil { - return nil, err - } - - req, err := http.NewRequest("GET", grafanaURL, nil) +var getFolderById = func(client *gclient.GrafanaHTTPAPI, folderId int64) (*models.Folder, error) { + params := folders.NewGetFolderByIDParams().WithFolderID(folderId) + folderOk, err := client.Folders.GetFolderByID(params, nil) if err != nil { return nil, err } - - if grafanaToken, ok := getGrafanaToken(); ok { - req.Header.Set("Authorization", "Bearer "+grafanaToken) - } - - resp, err := client.Do(req) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - switch resp.StatusCode { - case http.StatusNotFound: - return nil, fmt.Errorf("NOT FOUND") - default: - if resp.StatusCode >= 400 { - return nil, errors.New(resp.Status) - } - } - - data, err := io.ReadAll(resp.Body) - if err != nil { - return nil, err - } - - var f Folder - if err := json.Unmarshal(data, &f); err != nil { - return nil, err - } - return f, nil -} - -type Folder map[string]interface{} - -func (f *Folder) UID() string { - return (*f)["uid"].(string) -} - -func (f *Folder) ID() float64 { - return (*f)["id"].(float64) -} - -// toJSON returns JSON expected by Grafana API -func (f *Folder) toJSON() (string, error) { - j, err := json.MarshalIndent(f, "", " ") - if err != nil { - return "", err - } - return string(j), nil + return folderOk.GetPayload(), nil } diff --git a/pkg/grafana/folders_test.go b/pkg/grafana/folders_test.go index 6d265fe1..18efc72b 100644 --- a/pkg/grafana/folders_test.go +++ b/pkg/grafana/folders_test.go @@ -13,25 +13,36 @@ import ( func TestFolders(t *testing.T) { os.Setenv("GRAFANA_URL", GetUrl()) + grafanaClient, err := GetClient() + require.NoError(t, err) + + grizzly.ConfigureProviderRegistry( + []grizzly.Provider{ + NewProvider(grafanaClient), + }) + ticker := PingService(GetUrl()) defer ticker.Stop() + handler, err := grizzly.Registry.GetHandler((&FolderHandler{}).Kind()) + require.NoError(t, err) + t.Run("get remote folder - success", func(t *testing.T) { - resource, err := getRemoteFolder("abcdefghi") + resource, err := handler.GetByUID("abcdefghi") require.NoError(t, err) - require.Equal(t, resource.APIVersion(), "grizzly.grafana.com/v1alpha1") - require.Equal(t, resource.Name(), "abcdefghi") + require.Equal(t, "grizzly.grafana.com/v1alpha1", resource.APIVersion()) + require.Equal(t, "abcdefghi", resource.Name()) require.Len(t, resource.Spec(), 14) }) t.Run("get remote folder - not found", func(t *testing.T) { - _, err := getRemoteFolder("dummy") - require.EqualError(t, err, "couldn't fetch folder 'dummy' from remote: not found") + _, err := handler.GetByUID("dummy") + require.ErrorContains(t, err, "couldn't fetch folder 'dummy' from remote: not found") }) t.Run("get folders list", func(t *testing.T) { - resources, err := getRemoteFolderList() + resources, err := handler.ListRemote() require.NoError(t, err) require.NotNil(t, resources) @@ -47,24 +58,24 @@ func TestFolders(t *testing.T) { err = json.Unmarshal(folder, &resource) require.NoError(t, err) - err = postFolder(resource) + err = handler.Add(resource) require.NoError(t, err) - remoteFolder, err := getRemoteFolder("newFolder") + remoteFolder, err := handler.GetByUID("newFolder") require.NoError(t, err) require.NotNil(t, remoteFolder) - require.Equal(t, remoteFolder.Spec()["url"], "/dashboards/f/newFolder/new-folder") + require.Equal(t, "/dashboards/f/newFolder/new-folder", remoteFolder.Spec()["url"]) t.Run("put remote folder - update uid", func(t *testing.T) { remoteFolder.SetSpecString("uid", "dummyUid") - err := putFolder(*remoteFolder) + err := handler.Add(*remoteFolder) require.NoError(t, err) - updatedFolder, err := getRemoteFolder("dummyUid") + updatedFolder, err := handler.GetByUID("dummyUid") require.NoError(t, err) - require.Equal(t, updatedFolder.Spec()["uid"], "dummyUid") + require.Equal(t, "dummyUid", updatedFolder.Spec()["uid"]) }) }) @@ -79,10 +90,9 @@ func TestFolders(t *testing.T) { resource.SetSpecString("title", "Azure Data Explorer") - err = postFolder(resource) - - grafanaErr := err.(ErrNon200Response) - require.Error(t, err) - require.Equal(t, grafanaErr.Response.StatusCode, 409) + err = handler.Add(resource) + var non200ResponseErr ErrNon200Response + require.ErrorAs(t, err, &non200ResponseErr) + require.Equal(t, 409, non200ResponseErr.Response.StatusCode) }) } diff --git a/pkg/grafana/provider.go b/pkg/grafana/provider.go index e6cdf8ec..77d0684e 100644 --- a/pkg/grafana/provider.go +++ b/pkg/grafana/provider.go @@ -3,34 +3,44 @@ package grafana import ( "path/filepath" + gclient "github.com/grafana/grafana-openapi-client-go/client" "github.com/grafana/grizzly/pkg/grizzly" ) -// Provider defines a Grafana Provider -type Provider struct{} +// Provider is a grizzly.Provider implementation for Grafana. +type Provider struct { + client *gclient.GrafanaHTTPAPI +} + +// NewProvider instantiates a new Provider. +func NewProvider(client *gclient.GrafanaHTTPAPI) *Provider { + return &Provider{ + client: client, + } +} // Group returns the group name of the Grafana provider -func (p *Provider) Group() string { +func (p Provider) Group() string { return "grizzly.grafana.com" } // Version returns the version of this provider -func (p *Provider) Version() string { +func (p Provider) Version() string { return "v1alpha1" } // APIVersion returns the group and version of this provider -func (p *Provider) APIVersion() string { +func (p Provider) APIVersion() string { return filepath.Join(p.Group(), p.Version()) } // GetHandlers identifies the handlers for the Grafana provider -func (p *Provider) GetHandlers() []grizzly.Handler { +func (p Provider) GetHandlers() []grizzly.Handler { return []grizzly.Handler{ - NewDatasourceHandler(*p), - NewFolderHandler(*p), - NewDashboardHandler(*p), - NewRuleHandler(*p), - NewSyntheticMonitoringHandler(*p), + NewDatasourceHandler(p), + NewFolderHandler(p), + NewDashboardHandler(p), + NewRuleHandler(p), + NewSyntheticMonitoringHandler(p), } } diff --git a/pkg/grafana/rules.go b/pkg/grafana/rules.go index a0a62a82..e108e311 100644 --- a/pkg/grafana/rules.go +++ b/pkg/grafana/rules.go @@ -11,13 +11,6 @@ import ( "gopkg.in/yaml.v3" ) -const ( - cortexApiKey = "CORTEX_API_KEY" - cortexAddress = "CORTEX_ADDRESS" - cortexTenantID = "CORTEX_TENANT_ID" - backenTypeCortex = "cortex" -) - var cortexTool = func(args ...string) ([]byte, error) { path := os.Getenv("CORTEXTOOL_PATH") if path == "" { diff --git a/pkg/grafana/rules_test.go b/pkg/grafana/rules_test.go index 84be5d9f..6e55ccf1 100644 --- a/pkg/grafana/rules_test.go +++ b/pkg/grafana/rules_test.go @@ -6,6 +6,7 @@ import ( "testing" "github.com/grafana/grizzly/pkg/grizzly" + . "github.com/grafana/grizzly/pkg/internal/testutil" "github.com/stretchr/testify/require" "gopkg.in/yaml.v3" ) @@ -13,10 +14,14 @@ import ( var errCortextoolClient = errors.New("error coming from cortextool client") func TestRules(t *testing.T) { + os.Setenv("GRAFANA_URL", GetUrl()) + + grafanaClient, err := GetClient() + require.NoError(t, err) grizzly.ConfigureProviderRegistry( []grizzly.Provider{ - &Provider{}, + NewProvider(grafanaClient), }) t.Run("get remote rule group", func(t *testing.T) { @@ -108,7 +113,7 @@ func TestRules(t *testing.T) { handler := RuleHandler{} uid, err := handler.GetUID(resource) require.NoError(t, err) - require.Equal(t, uid, "test_namespace.test") + require.Equal(t, "test_namespace.test", uid) }) } @@ -128,16 +133,3 @@ func mockCortexTool(t *testing.T, file string, err error) { cortexTool = origCorexTool }) } - -type mockCTClient struct { - rules []byte - err error -} - -func (m mockCTClient) listRules() ([]byte, error) { - return m.rules, m.err -} - -func (m mockCTClient) writeRules(namespace, fileName string) error { - return m.err -} diff --git a/pkg/grafana/synthetic-monitoring_test.go b/pkg/grafana/synthetic-monitoring_test.go index 2ae3f923..bf32e5a5 100644 --- a/pkg/grafana/synthetic-monitoring_test.go +++ b/pkg/grafana/synthetic-monitoring_test.go @@ -11,9 +11,12 @@ import ( func TestSyntheticMonitoring(t *testing.T) { + client, err := GetClient() + require.NoError(t, err) + grizzly.ConfigureProviderRegistry( []grizzly.Provider{ - &Provider{}, + &Provider{client: client}, }) t.Run("Check getUID is functioning correctly", func(t *testing.T) { @@ -26,7 +29,7 @@ func TestSyntheticMonitoring(t *testing.T) { handler := SyntheticMonitoringHandler{} uid, err := handler.GetUID(resource) require.NoError(t, err) - require.Equal(t, uid, "http.test") + require.Equal(t, "http.test", uid) }) } diff --git a/pkg/grafana/utils.go b/pkg/grafana/utils.go index 29ba232e..f5ff6f5f 100644 --- a/pkg/grafana/utils.go +++ b/pkg/grafana/utils.go @@ -1,29 +1,44 @@ package grafana import ( + "encoding/json" "regexp" + + gclient "github.com/grafana/grafana-openapi-client-go/client" + "github.com/grafana/grafana-openapi-client-go/models" ) var folderURLRegex = regexp.MustCompile("/dashboards/f/([^/]+)") -const generalFolderId = 0 -const generalFolderUID = "general" - -func extractFolderUID(d DashboardWrapper) string { +func extractFolderUID(client *gclient.GrafanaHTTPAPI, d models.DashboardFullWithMeta) string { folderUid := d.Meta.FolderUID if folderUid == "" { urlPaths := folderURLRegex.FindStringSubmatch(d.Meta.FolderURL) if len(urlPaths) == 0 { - if d.FolderID == generalFolderId { + if d.Meta.FolderID == generalFolderId { return generalFolderUID } - folder, err := getFolderById(d.FolderID) + folder, err := getFolderById(client, d.Meta.FolderID) if err != nil { return "" } - return folder["uid"].(string) + return folder.UID } folderUid = urlPaths[1] } return folderUid } + +func structToMap(s interface{}) (map[string]interface{}, error) { + jsonData, err := json.Marshal(s) + if err != nil { + return nil, err + } + + var result map[string]interface{} + if err := json.Unmarshal(jsonData, &result); err != nil { + return nil, err + } + + return result, nil +} diff --git a/pkg/grafana/utils_test.go b/pkg/grafana/utils_test.go index be40dc77..8072c87d 100644 --- a/pkg/grafana/utils_test.go +++ b/pkg/grafana/utils_test.go @@ -1,36 +1,62 @@ package grafana import ( - "github.com/stretchr/testify/require" + "os" "testing" + + gclient "github.com/grafana/grafana-openapi-client-go/client" + "github.com/grafana/grafana-openapi-client-go/models" + "github.com/grafana/grizzly/pkg/grizzly" + . "github.com/grafana/grizzly/pkg/internal/testutil" + "github.com/stretchr/testify/require" ) func TestExtractFolderUID(t *testing.T) { + os.Setenv("GRAFANA_URL", GetUrl()) + + client, err := GetClient() + require.NoError(t, err) + + grizzly.ConfigureProviderRegistry( + []grizzly.Provider{ + &Provider{client: client}, + }) + t.Run("extract folder uid successfully - uid exists", func(t *testing.T) { - dashboardWrapper := DashboardWrapper{} - dashboardWrapper.Meta.FolderUID = "sample" - uid := extractFolderUID(dashboardWrapper) + meta := models.DashboardMeta{ + FolderUID: "sample", + } + dashboard := models.DashboardFullWithMeta{ + Meta: &meta, + } + uid := extractFolderUID(client, dashboard) require.Equal(t, "sample", uid) }) t.Run("extract folder uid successfully - url exists", func(t *testing.T) { - dashboardWrapper := DashboardWrapper{} - url := "/dashboards/f/sample/special-sample-folder" - dashboardWrapper.Meta.FolderURL = url - uid := extractFolderUID(dashboardWrapper) + meta := models.DashboardMeta{ + FolderURL: "/dashboards/f/sample/special-sample-folder", + } + dashboard := models.DashboardFullWithMeta{ + Meta: &meta, + } + uid := extractFolderUID(client, dashboard) require.Equal(t, "sample", uid) }) t.Run("extract folder uid - empty uid returned", func(t *testing.T) { - dashboardWrapper := DashboardWrapper{ + meta := models.DashboardMeta{ FolderID: 1, } - getFolderById = func(folderId int64) (Folder, error) { - return Folder{ - "uid": "12345", + dashboard := models.DashboardFullWithMeta{ + Meta: &meta, + } + getFolderById = func(client *gclient.GrafanaHTTPAPI, folderId int64) (*models.Folder, error) { + return &models.Folder{ + UID: "12345", }, nil } - uid := extractFolderUID(dashboardWrapper) + uid := extractFolderUID(client, dashboard) require.Equal(t, "12345", uid) }) } diff --git a/pkg/grizzly/workflow_test.go b/pkg/grizzly/workflow_test.go index d3168782..fd64bf02 100644 --- a/pkg/grizzly/workflow_test.go +++ b/pkg/grizzly/workflow_test.go @@ -15,9 +15,12 @@ import ( func TestPull(t *testing.T) { os.Setenv("GRAFANA_URL", GetUrl()) + grafanaClient, err := grafana.GetClient() + require.NoError(t, err) + grizzly.ConfigureProviderRegistry( []grizzly.Provider{ - &grafana.Provider{}, + grafana.NewProvider(grafanaClient), }) ticker := PingService(GetUrl())