Secrets: make operations sync (#107732)

* Secrets: make operations sync

* k8s gen / update query to list secure values to include the version

* always store new version of a secret

* make update-workspace

* go mod tidy

* update queries

* update queries

* improve and use testutils in decrypt_store_test

* fix broken test

* make update-workspace

* ./hack/update-codegen.sh secret

* update Test_SecureValueMetadataStorage_CreateAndRead

* undo dependency changes

* linter: fix remaining errors

---------

Co-authored-by: Matheus Macabu <macabu.matheus@gmail.com>
Co-authored-by: Matheus Macabu <macabu@users.noreply.github.com>
pull/107888/head
Bruno 2 weeks ago committed by GitHub
parent ded7912ea3
commit 8283d35e56
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 54
      pkg/apis/secret/go.mod
  2. 209
      pkg/apis/secret/go.sum
  3. 26
      pkg/apis/secret/v0alpha1/register.go
  4. 31
      pkg/apis/secret/v0alpha1/secure_value.go
  5. 21
      pkg/apis/secret/v0alpha1/zz_generated.openapi.go
  6. 1
      pkg/apis/secret/v0alpha1/zz_generated.openapi_violation_exceptions.list
  7. 64
      pkg/registry/apis/secret/contracts/outbox_queue.go
  8. 7
      pkg/registry/apis/secret/contracts/secure_value.go
  9. 239
      pkg/registry/apis/secret/service/secure_value.go
  10. 94
      pkg/registry/apis/secret/service/secure_value_test.go
  11. 66
      pkg/registry/apis/secret/testutils/testutils.go
  12. 44
      pkg/registry/apis/secret/worker/metrics.go
  13. 274
      pkg/registry/apis/secret/worker/worker.go
  14. 246
      pkg/registry/apis/secret/worker/worker_test.go
  15. 3
      pkg/registry/backgroundsvcs/background_services.go
  16. 4
      pkg/server/wire.go
  17. 78
      pkg/server/wire_gen.go
  18. 14
      pkg/storage/secret/metadata/data/secure_value_create.sql
  19. 4
      pkg/storage/secret/metadata/data/secure_value_delete.sql
  20. 10
      pkg/storage/secret/metadata/data/secure_value_get_latest_version.sql
  21. 12
      pkg/storage/secret/metadata/data/secure_value_list.sql
  22. 6
      pkg/storage/secret/metadata/data/secure_value_listByName.sql
  23. 33
      pkg/storage/secret/metadata/data/secure_value_outbox_append.sql
  24. 5
      pkg/storage/secret/metadata/data/secure_value_outbox_delete.sql
  25. 6
      pkg/storage/secret/metadata/data/secure_value_outbox_fetch_message_ids.sql
  26. 8
      pkg/storage/secret/metadata/data/secure_value_outbox_query_timestamp.sql
  27. 19
      pkg/storage/secret/metadata/data/secure_value_outbox_receiveN.sql
  28. 7
      pkg/storage/secret/metadata/data/secure_value_outbox_update_receive_count.sql
  29. 14
      pkg/storage/secret/metadata/data/secure_value_read.sql
  30. 11
      pkg/storage/secret/metadata/data/secure_value_read_for_decrypt.sql
  31. 8
      pkg/storage/secret/metadata/data/secure_value_set_version_to_active.sql
  32. 9
      pkg/storage/secret/metadata/data/secure_value_set_version_to_inactive.sql
  33. 30
      pkg/storage/secret/metadata/data/secure_value_update.sql
  34. 8
      pkg/storage/secret/metadata/data/secure_value_updateExternalId.sql
  35. 8
      pkg/storage/secret/metadata/data/secure_value_updateStatus.sql
  36. 166
      pkg/storage/secret/metadata/decrypt_store_test.go
  37. 5
      pkg/storage/secret/metadata/keeper_store_test.go
  38. 80
      pkg/storage/secret/metadata/metrics/metrics.go
  39. 397
      pkg/storage/secret/metadata/outbox_store.go
  40. 269
      pkg/storage/secret/metadata/outbox_store_test.go
  41. 117
      pkg/storage/secret/metadata/query.go
  42. 212
      pkg/storage/secret/metadata/query_test.go
  43. 38
      pkg/storage/secret/metadata/secure_value_model.go
  44. 430
      pkg/storage/secret/metadata/secure_value_store.go
  45. 16
      pkg/storage/secret/metadata/secure_value_store_test.go
  46. 8
      pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-not-null.sql
  47. 6
      pkg/storage/secret/metadata/testdata/mysql--secure_value_create-create-null.sql
  48. 4
      pkg/storage/secret/metadata/testdata/mysql--secure_value_delete-delete.sql
  49. 10
      pkg/storage/secret/metadata/testdata/mysql--secure_value_get_latest_version-get latest secure value version.sql
  50. 10
      pkg/storage/secret/metadata/testdata/mysql--secure_value_list-list.sql
  51. 6
      pkg/storage/secret/metadata/testdata/mysql--secure_value_listByName-list.sql
  52. 21
      pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-all-fields-present.sql
  53. 19
      pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-encrypted-secret.sql
  54. 19
      pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-external-id.sql
  55. 19
      pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_append-no-keeper-name.sql
  56. 5
      pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_delete-basic.sql
  57. 6
      pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_fetch_message_ids-basic.sql
  58. 19
      pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_receiveN-basic.sql
  59. 7
      pkg/storage/secret/metadata/testdata/mysql--secure_value_outbox_update_receive_count-update-receive-count.sql
  60. 12
      pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read-for-update.sql
  61. 12
      pkg/storage/secret/metadata/testdata/mysql--secure_value_read-read.sql
  62. 9
      pkg/storage/secret/metadata/testdata/mysql--secure_value_read_for_decrypt-read-for-decrypt.sql
  63. 8
      pkg/storage/secret/metadata/testdata/mysql--secure_value_set_version_to_active-set secure value version to active.sql
  64. 22
      pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-not-null.sql
  65. 18
      pkg/storage/secret/metadata/testdata/mysql--secure_value_update-update-null.sql
  66. 6
      pkg/storage/secret/metadata/testdata/mysql--secure_value_updateExternalId-updateExternalId.sql
  67. 8
      pkg/storage/secret/metadata/testdata/mysql--secure_value_updateStatus-updateStatus.sql
  68. 8
      pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-not-null.sql
  69. 6
      pkg/storage/secret/metadata/testdata/postgres--secure_value_create-create-null.sql
  70. 4
      pkg/storage/secret/metadata/testdata/postgres--secure_value_delete-delete.sql
  71. 10
      pkg/storage/secret/metadata/testdata/postgres--secure_value_get_latest_version-get latest secure value version.sql
  72. 10
      pkg/storage/secret/metadata/testdata/postgres--secure_value_list-list.sql
  73. 6
      pkg/storage/secret/metadata/testdata/postgres--secure_value_listByName-list.sql
  74. 21
      pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-all-fields-present.sql
  75. 19
      pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-encrypted-secret.sql
  76. 19
      pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-external-id.sql
  77. 19
      pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_append-no-keeper-name.sql
  78. 5
      pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_delete-basic.sql
  79. 6
      pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_fetch_message_ids-basic.sql
  80. 19
      pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_receiveN-basic.sql
  81. 7
      pkg/storage/secret/metadata/testdata/postgres--secure_value_outbox_update_receive_count-update-receive-count.sql
  82. 12
      pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read-for-update.sql
  83. 12
      pkg/storage/secret/metadata/testdata/postgres--secure_value_read-read.sql
  84. 9
      pkg/storage/secret/metadata/testdata/postgres--secure_value_read_for_decrypt-read-for-decrypt.sql
  85. 8
      pkg/storage/secret/metadata/testdata/postgres--secure_value_set_version_to_active-set secure value version to active.sql
  86. 22
      pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-not-null.sql
  87. 18
      pkg/storage/secret/metadata/testdata/postgres--secure_value_update-update-null.sql
  88. 6
      pkg/storage/secret/metadata/testdata/postgres--secure_value_updateExternalId-updateExternalId.sql
  89. 8
      pkg/storage/secret/metadata/testdata/postgres--secure_value_updateStatus-updateStatus.sql
  90. 8
      pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-not-null.sql
  91. 6
      pkg/storage/secret/metadata/testdata/sqlite--secure_value_create-create-null.sql
  92. 4
      pkg/storage/secret/metadata/testdata/sqlite--secure_value_delete-delete.sql
  93. 10
      pkg/storage/secret/metadata/testdata/sqlite--secure_value_get_latest_version-get latest secure value version.sql
  94. 10
      pkg/storage/secret/metadata/testdata/sqlite--secure_value_list-list.sql
  95. 6
      pkg/storage/secret/metadata/testdata/sqlite--secure_value_listByName-list.sql
  96. 21
      pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-all-fields-present.sql
  97. 19
      pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-encrypted-secret.sql
  98. 19
      pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-external-id.sql
  99. 19
      pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_append-no-keeper-name.sql
  100. 5
      pkg/storage/secret/metadata/testdata/sqlite--secure_value_outbox_delete-basic.sql
  101. Some files were not shown because too many files have changed in this diff Show More

@ -9,88 +9,36 @@ require (
google.golang.org/protobuf v1.36.6 google.golang.org/protobuf v1.36.6
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
k8s.io/apimachinery v0.33.2 k8s.io/apimachinery v0.33.2
k8s.io/apiserver v0.33.2
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738
) )
require ( require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v5 v5.0.2 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/coreos/go-semver v0.3.1 // indirect
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/logr v1.4.2 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.9 // indirect github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/kylelemons/godebug v1.1.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/onsi/ginkgo/v2 v2.22.2 // indirect
github.com/onsi/gomega v1.36.2 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.64.0 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/spf13/pflag v1.0.6 // indirect
github.com/stretchr/objx v0.5.2 // indirect github.com/stretchr/objx v0.5.2 // indirect
github.com/x448/float16 v0.8.4 // indirect github.com/x448/float16 v0.8.4 // indirect
go.etcd.io/bbolt v1.4.0 // indirect
go.etcd.io/etcd/api/v3 v3.5.21 // indirect
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
go.etcd.io/etcd/client/v3 v3.5.21 // indirect
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
go.opentelemetry.io/otel v1.36.0 // indirect go.opentelemetry.io/otel v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.36.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 // indirect
go.opentelemetry.io/otel/metric v1.36.0 // indirect
go.opentelemetry.io/otel/sdk v1.36.0 // indirect
go.opentelemetry.io/otel/trace v1.36.0 // indirect
go.opentelemetry.io/proto/otlp v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.uber.org/zap v1.27.0 // indirect
golang.org/x/net v0.41.0 // indirect golang.org/x/net v0.41.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sys v0.33.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/term v0.32.0 // indirect
golang.org/x/text v0.26.0 // indirect golang.org/x/text v0.26.0 // indirect
golang.org/x/time v0.11.0 // indirect
golang.org/x/tools v0.34.0 // indirect
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/inf.v0 v0.9.1 // indirect
k8s.io/api v0.33.2 // indirect
k8s.io/client-go v0.33.2 // indirect
k8s.io/component-base v0.33.2 // indirect
k8s.io/klog/v2 v2.130.1 // indirect k8s.io/klog/v2 v2.130.1 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect

@ -1,41 +1,11 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cenkalti/backoff/v5 v5.0.2 h1:rIfFVxEf1QsI7E1ZHfp/B4DF/6QBAUhmgkxc0H7Zss8=
github.com/cenkalti/backoff/v5 v5.0.2/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/coreos/go-semver v0.3.1 h1:yi21YpKnrx1gt5R+la8n5WgS0kCrsPp33dmEyHReZr4=
github.com/coreos/go-semver v0.3.1/go.mod h1:irMmmIw/7yzSRPWryHsK7EYSg09caPQL03VsM8rvUec=
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
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/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@ -46,156 +16,58 @@ github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
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 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e h1:BTKk7LHuG1kmAkucwTA7DuMbKpKvJTKrGdBmUNO4dfQ=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg= github.com/grafana/grafana/pkg/apimachinery v0.0.0-20250514132646-acbc7b54ed9e/go.mod h1:IA4SOwun8QyST9c5UNs/fN37XL6boXXDvRYFcFwbipg=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI=
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340 h1:uGoIog/wiQHI9GAxXO5TJbT0wWKH3O9HhOJW1F9c3fY=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.1-0.20191002090509-6af20e3a5340/go.mod h1:3bDW6wMZJB7tiONtC/1Xpicra6Wp5GgbTbQWCbI5fkc=
github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 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/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 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 h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.22.2 h1:/3X8Panh8/WwhU/3Ssa6rCKqPLuAkVY2I0RoyDLySlU=
github.com/onsi/ginkgo/v2 v2.22.2/go.mod h1:oeMosUL+8LtarXBHu/c0bx2D/K9zyQ6uX3cTyztHwsk=
github.com/onsi/gomega v1.36.2 h1:koNYke6TVk6ZmnyHrCXba/T/MoLBXFjeC1PtvYgw0A8=
github.com/onsi/gomega v1.36.2/go.mod h1:DdwyADRjrc825LhMEkD76cHR5+pUnjhUN8GlHlRPHzY=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v0.9.2/go.mod h1:OsXs2jCmiKlQ1lTBmv21f2mNfw4xf/QclQDMrYNZzcM=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.0.0-20181126121408-4724e9255275/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
github.com/prometheus/common v0.64.0 h1:pdZeA+g617P7oGv1CzdTzyeShxAGrTBsolKNOLQPGO4=
github.com/prometheus/common v0.64.0/go.mod h1:0gZns+BLRQ3V6NdaerOhMbwwRbNh9hkGINtQAsP5GS8=
github.com/prometheus/procfs v0.0.0-20181204211112-1dc9a6cbc91a/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js=
github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75 h1:6fotK7otjonDflCTK0BCfls4SPy3NcCVb5dqqmbRknE=
github.com/tmc/grpc-websocket-proxy v0.0.0-20220101234140-673ab2c3ae75/go.mod h1:KO6IkyS8Y3j8OdNO85qEYBsRPuteD+YciPomcXdrMnk=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510 h1:S2dVYn90KE98chqDkyE9Z4N61UnQd+KOfgp5Iu53llk=
github.com/xiang90/probing v0.0.0-20221125231312-a49e3df8f510/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
go.etcd.io/etcd/api/v3 v3.5.21 h1:A6O2/JDb3tvHhiIz3xf9nJ7REHvtEFJJ3veW3FbCnS8=
go.etcd.io/etcd/api/v3 v3.5.21/go.mod h1:c3aH5wcvXv/9dqIw2Y810LDXJfhSYdHQ0vxmP3CCHVY=
go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoBqUTc=
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
go.etcd.io/etcd/client/v2 v2.305.21 h1:eLiFfexc2mE+pTLz9WwnoEsX5JTTpLCYVivKkmVXIRA=
go.etcd.io/etcd/client/v2 v2.305.21/go.mod h1:OKkn4hlYNf43hpjEM3Ke3aRdUkhSl8xjKjSf8eCq2J8=
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
go.etcd.io/etcd/pkg/v3 v3.5.21 h1:jUItxeKyrDuVuWhdh0HtjUANwyuzcb7/FAeUfABmQsk=
go.etcd.io/etcd/pkg/v3 v3.5.21/go.mod h1:wpZx8Egv1g4y+N7JAsqi2zoUiBIUWznLjqJbylDjWgU=
go.etcd.io/etcd/raft/v3 v3.5.21 h1:dOmE0mT55dIUsX77TKBLq+RgyumsQuYeiRQnW/ylugk=
go.etcd.io/etcd/raft/v3 v3.5.21/go.mod h1:fmcuY5R2SNkklU4+fKVBQi2biVp5vafMrWUEj4TJ4Cs=
go.etcd.io/etcd/server/v3 v3.5.21 h1:9w0/k12majtgarGmlMVuhwXRI2ob3/d1Ik3X5TKo0yU=
go.etcd.io/etcd/server/v3 v3.5.21/go.mod h1:G1mOzdwuzKT1VRL7SqRchli/qcFrtLBTAQ4lV20sXXo=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0 h1:x7wzEgXfnzJcHDwStJT+mxOz4etr2EcexjqhBvmoakw=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.60.0/go.mod h1:rg+RlpR5dKwaS95IyyZqj5Wd4E13lk/msnTS0Xl9lJM=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0 h1:dNzwXjZKpMpE2JhmO+9HsPl42NIXFIFSUSSs0fiqra0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.36.0/go.mod h1:90PoxvaEB5n6AOdZvi+yWJQoE95U8Dhhw2bSyRqnTD0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0 h1:JgtbA0xkWHnTmYk7YusopJFX6uleBmAuZ8n05NEh8nQ=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.36.0/go.mod h1:179AK5aar5R3eS9FucPy6rggvU0g52cvKId8pv4+v0c=
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
@ -204,139 +76,58 @@ go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFw
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
go.opentelemetry.io/proto/otlp v1.6.0 h1:jQjP+AQyTf+Fe7OKj/MfkDrmK4MNVtw2NpXsf9fefDI=
go.opentelemetry.io/proto/otlp v1.6.0/go.mod h1:cicgGehlFuNdgZkcALOCh3VE6K/u2tAjzlRhDwmVpZc=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI=
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 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/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 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-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-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.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 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-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/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 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-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 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-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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-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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb h1:ITgPrl429bc6+2ZraNSzMDk3I95nmQln2fuPstKwFDE=
google.golang.org/genproto v0.0.0-20250303144028-a0af3efb3deb/go.mod h1:sAo5UzpjUwgFBCzupwhcLcxHVDK7vG5IqI30YnwX2eE=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237 h1:Kog3KlB4xevJlAcbbbzPfRG0+X9fdoGM+UBRKVz6Wr0=
google.golang.org/genproto/googleapis/api v0.0.0-20250519155744-55703ea1f237/go.mod h1:ezi0AVyMKDWy5xAncvjLWH7UcLBB5n7y2fQ8MzjJcto=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34= google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237 h1:cJfm9zPbe1e873mHJzmQ1nwVEeRDU/T1wXDK2kUSU34=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A= google.golang.org/genproto/googleapis/rpc v0.0.0-20250519155744-55703ea1f237/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
google.golang.org/grpc v1.18.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok= google.golang.org/grpc v1.73.0 h1:VIWSmpI2MegBtTuFt5/JWy2oXxtjJ/e89Z70ImfD2ok=
google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc= google.golang.org/grpc v1.73.0/go.mod h1:50sbHOUqWoCQGI8V2HQLJM0B+LMlIUjNSZmow7EVBQc=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
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=
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
k8s.io/api v0.33.2 h1:YgwIS5jKfA+BZg//OQhkJNIfie/kmRsO0BmNaVSimvY=
k8s.io/api v0.33.2/go.mod h1:fhrbphQJSM2cXzCWgqU29xLDuks4mu7ti9vveEnpSXs=
k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY= k8s.io/apimachinery v0.33.2 h1:IHFVhqg59mb8PJWTLi8m1mAoepkUNYmptHsV+Z1m5jY=
k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= k8s.io/apimachinery v0.33.2/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM=
k8s.io/apiserver v0.33.2 h1:KGTRbxn2wJagJowo29kKBp4TchpO1DRO3g+dB/KOJN4=
k8s.io/apiserver v0.33.2/go.mod h1:9qday04wEAMLPWWo9AwqCZSiIn3OYSZacDyu/AcoM/M=
k8s.io/client-go v0.33.2 h1:z8CIcc0P581x/J1ZYf4CNzRKxRvQAwoAolYPbtQes+E=
k8s.io/client-go v0.33.2/go.mod h1:9mCgT4wROvL948w6f6ArJNb7yQd7QsvqavDeZHvNmHo=
k8s.io/component-base v0.33.2 h1:sCCsn9s/dG3ZrQTX/Us0/Sx2R0G5kwa0wbZFYoVp/+0=
k8s.io/component-base v0.33.2/go.mod h1:/41uw9wKzuelhN+u+/C59ixxf4tYQKW7p32ddkYNe2k=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 h1:jpcvIRr3GLoUoEKRkHKSmGjxb6lWwrBlJsXc+eUYQHM=
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=

@ -4,10 +4,8 @@ import (
"fmt" "fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/registry/generic"
"github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apimachinery/utils"
) )
@ -33,7 +31,6 @@ var SecureValuesResourceInfo = utils.NewResourceInfo(
{Name: "Description", Type: "string", Format: "string", Description: "Short description that explains the purpose of this SecureValue"}, {Name: "Description", Type: "string", Format: "string", Description: "Short description that explains the purpose of this SecureValue"},
{Name: "Keeper", Type: "string", Format: "string", Description: "Storage of the secure value"}, {Name: "Keeper", Type: "string", Format: "string", Description: "Storage of the secure value"},
{Name: "Ref", Type: "string", Format: "string", Description: "If present, the reference to a secret"}, {Name: "Ref", Type: "string", Format: "string", Description: "If present, the reference to a secret"},
{Name: "Status", Type: "string", Format: "string", Description: "The status of the secure value"},
}, },
// Decodes the object into a concrete type. Return order in the slice must be the same as in `Definition`. // Decodes the object into a concrete type. Return order in the slice must be the same as in `Definition`.
Reader: func(obj any) ([]interface{}, error) { Reader: func(obj any) ([]interface{}, error) {
@ -44,7 +41,6 @@ var SecureValuesResourceInfo = utils.NewResourceInfo(
r.Spec.Description, r.Spec.Description,
r.Spec.Keeper, r.Spec.Keeper,
r.Spec.Ref, r.Spec.Ref,
r.Status.Phase,
}, nil }, nil
} }
@ -92,13 +88,6 @@ var (
AddToScheme = localSchemeBuilder.AddToScheme AddToScheme = localSchemeBuilder.AddToScheme
) )
// Adds the status phase to the selectable fields, besides the generic metadata name and namespace.
func SelectableSecureValueFields(obj *SecureValue) fields.Set {
return generic.MergeFieldsSets(generic.ObjectMetaFieldsSet(&obj.ObjectMeta, false), fields.Set{
"status.phase": string(obj.Status.Phase),
})
}
// Adds the list of known types to the given scheme. // Adds the list of known types to the given scheme.
func AddKnownTypes(scheme *runtime.Scheme, version string) error { func AddKnownTypes(scheme *runtime.Scheme, version string) error {
// TODO: do we need a type for the secure value decrypt? // TODO: do we need a type for the secure value decrypt?
@ -112,20 +101,5 @@ func AddKnownTypes(scheme *runtime.Scheme, version string) error {
// &secretV0.SecureValueActivityList{}, // &secretV0.SecureValueActivityList{},
) )
err := scheme.AddFieldLabelConversionFunc(
SecureValuesResourceInfo.GroupVersionKind(),
func(label, value string) (string, string, error) {
fieldSet := SelectableSecureValueFields(&SecureValue{})
for key := range fieldSet {
if label == key {
return label, value, nil
}
}
return "", "", fmt.Errorf("field label not supported for %s: %s", SecureValuesResourceInfo.GroupVersionKind(), label)
},
)
if err != nil {
return err
}
return nil return nil
} }

@ -11,41 +11,18 @@ type SecureValue struct {
// Standard object's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. // Standard object's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// +optional // +optional
metav1.ObjectMeta `json:"metadata,omitempty"` metav1.ObjectMeta `json:"metadata"`
// This is the actual secure value schema. // This is the actual secure value schema.
Spec SecureValueSpec `json:"spec"` Spec SecureValueSpec `json:"spec"`
// Read-only observed status of the `SecureValue`. // Read-only observed status of the `SecureValue`.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status
Status SecureValueStatus `json:"status,omitempty"` Status SecureValueStatus `json:"status"`
} }
// +enum
type SecureValuePhase string
const (
// When the `SecureValue` is created, it will start in `Pending` phase to create the underlying secret asynchronously.
SecureValuePhasePending SecureValuePhase = "Pending"
// If the creation of the secret is successful, it will move to the `Succeeded` phase.
SecureValuePhaseSucceeded SecureValuePhase = "Succeeded"
// If the creation of the secret fails, it will move to the `Failed` phase.
// Check the additional `status` fields for more information on what caused the failure.
// This state is unrecoverable.
SecureValuePhaseFailed SecureValuePhase = "Failed"
)
type SecureValueStatus struct { type SecureValueStatus struct {
// High-level summary of where the `SecureValue` is in its lifecycle. Version int64 `json:"version"`
// One of: `Pending`, `Succeeded` or `Failed`.
Phase SecureValuePhase `json:"phase"`
// A human readable message indicating details about why the `SecureValue` is in this phase.
// Only applicable if the `phase=Failed`.
// +optional
Message string `json:"message,omitempty"`
// +optional // +optional
ExternalID string `json:"externalId,omitempty"` ExternalID string `json:"externalId,omitempty"`
@ -94,7 +71,7 @@ type SecureValueList struct {
// Standard list's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`. // Standard list's metadata. It can only be one of `metav1.ObjectMeta` or `metav1.ListMeta`.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata // More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata
// +optional // +optional
metav1.ListMeta `json:"metadata,omitempty"` metav1.ListMeta `json:"metadata"`
// Slice containing all secure values. This will NOT output decrypted values. // Slice containing all secure values. This will NOT output decrypted values.
Items []SecureValue `json:"items"` Items []SecureValue `json:"items"`

@ -554,7 +554,7 @@ func schema_pkg_apis_secret_v0alpha1_SecureValue(ref common.ReferenceCallback) c
}, },
}, },
}, },
Required: []string{"spec"}, Required: []string{"spec", "status"},
}, },
}, },
Dependencies: []string{ Dependencies: []string{
@ -690,20 +690,11 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueStatus(ref common.ReferenceCallb
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Type: []string{"object"}, Type: []string{"object"},
Properties: map[string]spec.Schema{ Properties: map[string]spec.Schema{
"phase": { "version": {
SchemaProps: spec.SchemaProps{ SchemaProps: spec.SchemaProps{
Description: "High-level summary of where the `SecureValue` is in its lifecycle. One of: `Pending`, `Succeeded` or `Failed`.\n\nPossible enum values:\n - `\"Failed\"` If the creation of the secret fails, it will move to the `Failed` phase. Check the additional `status` fields for more information on what caused the failure. This state is unrecoverable.\n - `\"Pending\"` When the `SecureValue` is created, it will start in `Pending` phase to create the underlying secret asynchronously.\n - `\"Succeeded\"` If the creation of the secret is successful, it will move to the `Succeeded` phase.", Default: 0,
Default: "", Type: []string{"integer"},
Type: []string{"string"}, Format: "int64",
Format: "",
Enum: []interface{}{"Failed", "Pending", "Succeeded"},
},
},
"message": {
SchemaProps: spec.SchemaProps{
Description: "A human readable message indicating details about why the `SecureValue` is in this phase. Only applicable if the `phase=Failed`.",
Type: []string{"string"},
Format: "",
}, },
}, },
"externalId": { "externalId": {
@ -713,7 +704,7 @@ func schema_pkg_apis_secret_v0alpha1_SecureValueStatus(ref common.ReferenceCallb
}, },
}, },
}, },
Required: []string{"phase"}, Required: []string{"version"},
}, },
}, },
} }

@ -6,3 +6,4 @@ API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alp
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,Azure API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,Azure
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,HashiCorp API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,KeeperSpec,HashiCorp
API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,SecureValueStatus,ExternalID API rule violation: names_match,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,SecureValueStatus,ExternalID
API rule violation: streaming_list_type_json_tags,github.com/grafana/grafana/pkg/apis/secret/v0alpha1,SecureValueList,ListMeta

@ -1,64 +0,0 @@
package contracts
import (
"context"
)
type contextRequestIdKey struct{}
type OutboxMessageType string
func GetRequestId(ctx context.Context) string {
v := ctx.Value(contextRequestIdKey{})
requestId, ok := v.(string)
if !ok {
return ""
}
return requestId
}
func ContextWithRequestID(ctx context.Context, requestId string) context.Context {
return context.WithValue(ctx, contextRequestIdKey{}, requestId)
}
const (
CreateSecretOutboxMessage OutboxMessageType = "create"
UpdateSecretOutboxMessage OutboxMessageType = "update"
DeleteSecretOutboxMessage OutboxMessageType = "delete"
)
type AppendOutboxMessage struct {
RequestID string
Type OutboxMessageType
Name string
Namespace string
EncryptedSecret string
KeeperName *string
ExternalID *string
}
type OutboxMessage struct {
RequestID string
Type OutboxMessageType
MessageID int64
Name string
Namespace string
EncryptedSecret string
KeeperName *string
ExternalID *string
// How many times this message has been received
ReceiveCount int
Created int64
}
type OutboxQueue interface {
// Appends a message to the outbox queue
Append(ctx context.Context, message AppendOutboxMessage) (int64, error)
// Receives at most n messages from the outbox queue
ReceiveN(ctx context.Context, n uint) ([]OutboxMessage, error)
// Deletes a message from the outbox queue
Delete(ctx context.Context, messageID int64) error
// Increments the number of times each message has been received by 1. Must be atomic.
IncrementReceiveCount(ctx context.Context, messageIDs []int64) error
}

@ -32,10 +32,9 @@ type ReadOpts struct {
type SecureValueMetadataStorage interface { type SecureValueMetadataStorage interface {
Create(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) Create(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error)
Read(ctx context.Context, namespace xkube.Namespace, name string, opts ReadOpts) (*secretv0alpha1.SecureValue, error) Read(ctx context.Context, namespace xkube.Namespace, name string, opts ReadOpts) (*secretv0alpha1.SecureValue, error)
Update(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error)
Delete(ctx context.Context, namespace xkube.Namespace, name string) error
List(ctx context.Context, namespace xkube.Namespace) ([]secretv0alpha1.SecureValue, error) List(ctx context.Context, namespace xkube.Namespace) ([]secretv0alpha1.SecureValue, error)
SetStatus(ctx context.Context, namespace xkube.Namespace, name string, status secretv0alpha1.SecureValueStatus) error SetVersionToActive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error
SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, externalID ExternalID) error SetVersionToInactive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error
SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, version int64, externalID ExternalID) error
ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*DecryptSecureValue, error) ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*DecryptSecureValue, error)
} }

@ -5,10 +5,10 @@ import (
"fmt" "fmt"
claims "github.com/grafana/authlib/types" claims "github.com/grafana/authlib/types"
"github.com/grafana/grafana-app-sdk/logging"
"github.com/grafana/grafana/pkg/apimachinery/utils" "github.com/grafana/grafana/pkg/apimachinery/utils"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/tracectx"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace" "go.opentelemetry.io/otel/trace"
@ -19,8 +19,8 @@ type SecureValueService struct {
accessClient claims.AccessClient accessClient claims.AccessClient
database contracts.Database database contracts.Database
secureValueMetadataStorage contracts.SecureValueMetadataStorage secureValueMetadataStorage contracts.SecureValueMetadataStorage
outboxQueue contracts.OutboxQueue keeperMetadataStorage contracts.KeeperMetadataStorage
encryptionManager contracts.EncryptionManager keeperService contracts.KeeperService
} }
func ProvideSecureValueService( func ProvideSecureValueService(
@ -28,16 +28,16 @@ func ProvideSecureValueService(
accessClient claims.AccessClient, accessClient claims.AccessClient,
database contracts.Database, database contracts.Database,
secureValueMetadataStorage contracts.SecureValueMetadataStorage, secureValueMetadataStorage contracts.SecureValueMetadataStorage,
outboxQueue contracts.OutboxQueue, keeperMetadataStorage contracts.KeeperMetadataStorage,
encryptionManager contracts.EncryptionManager, keeperService contracts.KeeperService,
) *SecureValueService { ) *SecureValueService {
return &SecureValueService{ return &SecureValueService{
tracer: tracer, tracer: tracer,
accessClient: accessClient, accessClient: accessClient,
database: database, database: database,
secureValueMetadataStorage: secureValueMetadataStorage, secureValueMetadataStorage: secureValueMetadataStorage,
outboxQueue: outboxQueue, keeperMetadataStorage: keeperMetadataStorage,
encryptionManager: encryptionManager, keeperService: keeperService,
} }
} }
@ -48,43 +48,91 @@ func (s *SecureValueService) Create(ctx context.Context, sv *secretv0alpha1.Secu
attribute.String("actor", actorUID), attribute.String("actor", actorUID),
)) ))
defer span.End() defer span.End()
return s.createNewVersion(ctx, sv, actorUID)
}
sv.Status = secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhasePending, Message: "Creating secure value"} func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, bool, error) {
ctx, span := s.tracer.Start(ctx, "SecureValueService.Update", trace.WithAttributes(
var out *secretv0alpha1.SecureValue attribute.String("name", newSecureValue.GetName()),
attribute.String("namespace", newSecureValue.GetNamespace()),
attribute.String("actor", actorUID),
))
defer span.End()
encryptedSecret, err := s.encryptionManager.Encrypt(ctx, sv.Namespace, []byte(sv.Spec.Value.DangerouslyExposeAndConsumeValue())) if newSecureValue.Spec.Value == "" {
if err != nil { decrypted, err := s.secureValueMetadataStorage.ReadForDecrypt(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name)
return nil, fmt.Errorf("encrypting secure value secret: %w", err) if err != nil {
} return nil, false, fmt.Errorf("reading secure value secret: %+w", err)
}
// Specifically here so that the spans from the worker are not inside the transaction. // TODO: does this need to be for update?
requestID := tracectx.HexEncodeTraceFromContext(ctx) keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, newSecureValue.Namespace, newSecureValue.Spec.Keeper, contracts.ReadOpts{ForUpdate: true})
if err != nil {
return nil, false, fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Spec.Keeper, err)
}
if err := s.database.Transaction(ctx, func(ctx context.Context) error { keeper, err := s.keeperService.KeeperForConfig(keeperCfg)
createdSecureValue, err := s.secureValueMetadataStorage.Create(ctx, sv, actorUID)
if err != nil { if err != nil {
return fmt.Errorf("failed to create securevalue: %w", err) return nil, false, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", newSecureValue.Namespace, newSecureValue.Spec.Keeper, err)
} }
out = createdSecureValue logging.FromContext(ctx).Debug("retrieved keeper", "namespace", newSecureValue.Namespace, "keeperName", newSecureValue.Spec.Keeper, "type", keeperCfg.Type())
if _, err := s.outboxQueue.Append(ctx, contracts.AppendOutboxMessage{ secret, err := keeper.Expose(ctx, keeperCfg, newSecureValue.Namespace, contracts.ExternalID(decrypted.ExternalID))
RequestID: requestID, if err != nil {
Type: contracts.CreateSecretOutboxMessage, return nil, false, fmt.Errorf("reading secret value from keeper: %w", err)
Name: sv.Name,
Namespace: sv.Namespace,
EncryptedSecret: string(encryptedSecret),
KeeperName: sv.Spec.Keeper,
}); err != nil {
return fmt.Errorf("failed to append message to create secure value to outbox queue: %w", err)
} }
return nil newSecureValue.Spec.Value = secret
}); err != nil { }
return out, err
const updateIsSync = true
createdSv, err := s.createNewVersion(ctx, newSecureValue, actorUID)
return createdSv, updateIsSync, err
}
func (s *SecureValueService) createNewVersion(ctx context.Context, sv *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) {
createdSv, err := s.secureValueMetadataStorage.Create(ctx, sv, actorUID)
if err != nil {
return nil, fmt.Errorf("creating secure value: %w", err)
}
createdSv.Status = secretv0alpha1.SecureValueStatus{
Version: createdSv.Status.Version,
}
// TODO: does this need to be for update?
keeperCfg, err := s.keeperMetadataStorage.GetKeeperConfig(ctx, sv.Namespace, sv.Spec.Keeper, contracts.ReadOpts{ForUpdate: true})
if err != nil {
return nil, fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Spec.Keeper, err)
}
keeper, err := s.keeperService.KeeperForConfig(keeperCfg)
if err != nil {
return nil, fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", sv.Namespace, sv.Spec.Keeper, err)
}
logging.FromContext(ctx).Debug("retrieved keeper", "namespace", sv.Namespace, "keeperName", sv.Spec.Keeper, "type", keeperCfg.Type())
// TODO: can we stop using external id?
// TODO: store uses only the namespace and returns and id. It could be a kv instead.
// TODO: check that the encrypted store works with multiple versions
externalID, err := keeper.Store(ctx, keeperCfg, sv.Namespace, sv.Spec.Value.DangerouslyExposeAndConsumeValue())
if err != nil {
return nil, fmt.Errorf("storing secure value in keeper: %w", err)
}
createdSv.Status.ExternalID = string(externalID)
if err := s.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(sv.Namespace), sv.Name, createdSv.Status.Version, externalID); err != nil {
return nil, fmt.Errorf("setting secure value external id: %w", err)
}
if err := s.secureValueMetadataStorage.SetVersionToActive(ctx, xkube.Namespace(sv.Namespace), sv.Name, createdSv.Status.Version); err != nil {
return nil, fmt.Errorf("marking secure value version as active: %w", err)
} }
return out, nil // In a single query:
// TODO: set external id
// TODO: set to active
return createdSv, nil
} }
func (s *SecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error) { func (s *SecureValueService) Read(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error) {
@ -139,84 +187,6 @@ func (s *SecureValueService) List(ctx context.Context, namespace xkube.Namespace
}, nil }, nil
} }
func (s *SecureValueService) Update(ctx context.Context, newSecureValue *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, bool, error) {
ctx, span := s.tracer.Start(ctx, "SecureValueService.Create", trace.WithAttributes(
attribute.String("name", newSecureValue.GetName()),
attribute.String("namespace", newSecureValue.GetNamespace()),
attribute.String("actor", actorUID),
))
defer span.End()
// True when the effects of an update can be seen immediately.
// Never true in this case since updating a secure value is async.
const updateIsSync = false
var (
out *secretv0alpha1.SecureValue
encryptedSecret string
)
if newSecureValue.Spec.Value != "" {
buffer, err := s.encryptionManager.Encrypt(ctx, newSecureValue.Namespace, []byte(newSecureValue.Spec.Value.DangerouslyExposeAndConsumeValue()))
if err != nil {
return nil, false, fmt.Errorf("encrypting secure value secret: %w", err)
}
encryptedSecret = string(buffer)
}
// Especifically here so that the spans from the worker are not inside the transaction.
requestID := tracectx.HexEncodeTraceFromContext(ctx)
if err := s.database.Transaction(ctx, func(ctx context.Context) error {
sv, err := s.secureValueMetadataStorage.Read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{ForUpdate: true})
if err != nil {
return fmt.Errorf("fetching secure value: %+w", err)
}
if sv.Status.Phase == secretv0alpha1.SecureValuePhasePending {
return contracts.ErrSecureValueOperationInProgress
}
// Succeed immediately if the value is not going to be updated
if encryptedSecret == "" {
newSecureValue.Status = secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseSucceeded}
} else {
newSecureValue.Status = secretv0alpha1.SecureValueStatus{
Message: "Updating secure value",
Phase: secretv0alpha1.SecureValuePhasePending,
}
}
// Current implementation replaces everything passed in the spec, so it is not a PATCH. Do we want/need to support that?
updatedSecureValue, err := s.secureValueMetadataStorage.Update(ctx, newSecureValue, actorUID)
if err != nil {
return fmt.Errorf("failed to update secure value: %w", err)
}
out = updatedSecureValue
// Only the value needs to be updated asynchronously by the outbox worker
if encryptedSecret != "" {
if _, err := s.outboxQueue.Append(ctx, contracts.AppendOutboxMessage{
RequestID: requestID,
Type: contracts.UpdateSecretOutboxMessage,
Name: newSecureValue.Name,
Namespace: newSecureValue.Namespace,
EncryptedSecret: encryptedSecret,
KeeperName: newSecureValue.Spec.Keeper,
ExternalID: &updatedSecureValue.Status.ExternalID,
}); err != nil {
return fmt.Errorf("failed to append message to update secure value to outbox queue: %w", err)
}
}
return nil
}); err != nil {
return out, updateIsSync, err
}
return out, updateIsSync, nil
}
func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error) { func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespace, name string) (*secretv0alpha1.SecureValue, error) {
ctx, span := s.tracer.Start(ctx, "SecureValueService.Delete", trace.WithAttributes( ctx, span := s.tracer.Start(ctx, "SecureValueService.Delete", trace.WithAttributes(
attribute.String("name", name), attribute.String("name", name),
@ -224,45 +194,14 @@ func (s *SecureValueService) Delete(ctx context.Context, namespace xkube.Namespa
)) ))
defer span.End() defer span.End()
// Set inside of the transaction callback sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: true})
var out *secretv0alpha1.SecureValue if err != nil {
return nil, fmt.Errorf("fetching secure value: %+w", err)
// Especifically here so that the spans from the worker are not inside the transaction. }
requestID := tracectx.HexEncodeTraceFromContext(ctx)
if err := s.database.Transaction(ctx, func(ctx context.Context) error {
sv, err := s.secureValueMetadataStorage.Read(ctx, namespace, name, contracts.ReadOpts{ForUpdate: true})
if err != nil {
return fmt.Errorf("fetching secure value: %+w", err)
}
if sv.Status.Phase == secretv0alpha1.SecureValuePhasePending {
return contracts.ErrSecureValueOperationInProgress
}
sv.Status = secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhasePending, Message: "Deleting secure value"}
if err := s.secureValueMetadataStorage.SetStatus(ctx, namespace, name, sv.Status); err != nil {
return fmt.Errorf("setting secure value status phase: %+w", err)
}
if _, err := s.outboxQueue.Append(ctx, contracts.AppendOutboxMessage{
RequestID: requestID,
Type: contracts.DeleteSecretOutboxMessage,
Name: name,
Namespace: namespace.String(),
KeeperName: sv.Spec.Keeper,
ExternalID: &sv.Status.ExternalID,
}); err != nil {
return fmt.Errorf("appending delete secure value message to outbox queue: %+w", err)
}
out = sv
return nil if err := s.secureValueMetadataStorage.SetVersionToInactive(ctx, namespace, name, sv.Status.Version); err != nil {
}); err != nil { return nil, fmt.Errorf("setting secure value version to inactive: %+w", err)
return out, err
} }
return out, nil return sv, nil
} }

@ -0,0 +1,94 @@
package service_test
import (
"testing"
"github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/testutils"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/stretchr/testify/require"
)
func TestCrud(t *testing.T) {
t.Parallel()
t.Run("creating a secure value creates new versions", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
sv1, err := sut.CreateSv(t.Context())
require.NoError(t, err)
// Create the same secure value twice
input := sv1.DeepCopy()
input.Spec.Description = "d2"
input.Spec.Value = v0alpha1.NewExposedSecureValue("v2")
sv2, err := sut.CreateSv(t.Context(), testutils.CreateSvWithSv(input))
require.NoError(t, err)
require.True(t, sv2.Status.Version > sv1.Status.Version)
// Read the secure value
sv, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(sv2.Namespace), sv2.Name)
require.NoError(t, err)
// It should be the latest version
require.Equal(t, sv2.Namespace, sv.Namespace)
require.Equal(t, sv2.Name, sv.Name)
require.Equal(t, "d2", sv.Spec.Description)
require.Equal(t, sv2.Status.Version, sv.Status.Version)
})
t.Run("updating a secure value creates new versions", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
// Create a secure value
sv1, err := sut.CreateSv(t.Context())
require.NoError(t, err)
ns := sv1.Namespace
name := sv1.Name
// Update the secure value
input := sv1.DeepCopy()
input.Spec.Description = "d2"
sv2, err := sut.UpdateSv(t.Context(), input)
require.NoError(t, err)
// Read the secure value
sv3, err := sut.SecureValueService.Read(t.Context(), xkube.Namespace(ns), name)
require.NoError(t, err)
// Nothing has changed except for the updated field.
require.Equal(t, ns, sv2.Namespace)
require.Equal(t, name, sv2.Name)
require.True(t, sv2.Status.Version > sv1.Status.Version)
require.Equal(t, "d2", sv2.Spec.Description)
require.Equal(t, ns, sv3.Namespace)
require.Equal(t, name, sv3.Name)
require.Equal(t, sv2.Status.Version, sv3.Status.Version)
require.Equal(t, "d2", sv3.Spec.Description)
})
t.Run("deleting secure values", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
sv1, err := sut.CreateSv(t.Context())
require.NoError(t, err)
sv2, err := sut.DeleteSv(t.Context(), sv1.Namespace, sv1.Name)
require.NoError(t, err)
require.Equal(t, sv1.Namespace, sv2.Namespace)
require.Equal(t, sv1.Name, sv2.Name)
_, err = sut.SecureValueMetadataStorage.Read(t.Context(), xkube.Namespace(sv1.Namespace), sv1.Name, contracts.ReadOpts{})
require.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
})
}

@ -3,7 +3,6 @@ package testutils
import ( import (
"context" "context"
"testing" "testing"
"time"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption" encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption"
@ -12,11 +11,11 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats" "github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/decrypt"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
"github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper" "github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper/sqlkeeper"
"github.com/grafana/grafana/pkg/registry/apis/secret/service" "github.com/grafana/grafana/pkg/registry/apis/secret/service"
"github.com/grafana/grafana/pkg/registry/apis/secret/worker"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube" "github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/actest" "github.com/grafana/grafana/pkg/services/accesscontrol/actest"
@ -29,35 +28,28 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
type setupConfig struct { type SetupConfig struct {
workerCfg worker.Config KeeperService contracts.KeeperService
keeperService contracts.KeeperService AllowList map[string]struct{}
} }
func defaultSetupCfg() setupConfig { func defaultSetupCfg() SetupConfig {
return setupConfig{ return SetupConfig{}
workerCfg: worker.Config{
BatchSize: 10,
ReceiveTimeout: 1 * time.Second,
PollingInterval: time.Millisecond,
MaxMessageProcessingAttempts: 5,
},
}
} }
func WithWorkerConfig(cfg worker.Config) func(*setupConfig) { func WithKeeperService(keeperService contracts.KeeperService) func(*SetupConfig) {
return func(setupCfg *setupConfig) { return func(setupCfg *SetupConfig) {
setupCfg.workerCfg = cfg setupCfg.KeeperService = keeperService
} }
} }
func WithKeeperService(keeperService contracts.KeeperService) func(*setupConfig) { func WithMutateCfg(f func(*SetupConfig)) func(*SetupConfig) {
return func(setupCfg *setupConfig) { return func(cfg *SetupConfig) {
setupCfg.keeperService = keeperService f(cfg)
} }
} }
func Setup(t *testing.T, opts ...func(*setupConfig)) Sut { func Setup(t *testing.T, opts ...func(*SetupConfig)) Sut {
setupCfg := defaultSetupCfg() setupCfg := defaultSetupCfg()
for _, opt := range opts { for _, opt := range opts {
opt(&setupCfg) opt(&setupCfg)
@ -68,8 +60,6 @@ func Setup(t *testing.T, opts ...func(*setupConfig)) Sut {
database := database.ProvideDatabase(testDB, tracer) database := database.ProvideDatabase(testDB, tracer)
outboxQueue := metadata.ProvideOutboxQueue(database, tracer, nil)
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(database, tracer, features, nil) keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(database, tracer, features, nil)
@ -111,34 +101,24 @@ func Setup(t *testing.T, opts ...func(*setupConfig)) Sut {
var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper) var keeperService contracts.KeeperService = newKeeperServiceWrapper(sqlKeeper)
if setupCfg.keeperService != nil { if setupCfg.KeeperService != nil {
keeperService = setupCfg.keeperService keeperService = setupCfg.KeeperService
} }
secureValueService := service.ProvideSecureValueService(tracer, accessClient, database, secureValueMetadataStorage, outboxQueue, encryptionManager) secureValueService := service.ProvideSecureValueService(tracer, accessClient, database, secureValueMetadataStorage, keeperMetadataStorage, keeperService)
worker, err := worker.NewWorker( decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, setupCfg.AllowList)
setupCfg.workerCfg,
tracer, decryptStorage, err := metadata.ProvideDecryptStorage(features, tracer, keeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, nil)
database,
outboxQueue,
secureValueMetadataStorage,
keeperMetadataStorage,
keeperService,
encryptionManager,
features,
nil, // metrics
)
require.NoError(t, err) require.NoError(t, err)
return Sut{Worker: worker, SecureValueService: secureValueService, SecureValueMetadataStorage: secureValueMetadataStorage, OutboxQueue: outboxQueue, Database: database} return Sut{SecureValueService: secureValueService, SecureValueMetadataStorage: secureValueMetadataStorage, Database: database, DecryptStorage: decryptStorage}
} }
type Sut struct { type Sut struct {
Worker *worker.Worker
SecureValueService *service.SecureValueService SecureValueService *service.SecureValueService
SecureValueMetadataStorage contracts.SecureValueMetadataStorage SecureValueMetadataStorage contracts.SecureValueMetadataStorage
OutboxQueue contracts.OutboxQueue DecryptStorage contracts.DecryptStorage
Database *database.Database Database *database.Database
} }
@ -163,9 +143,7 @@ func (s *Sut) CreateSv(ctx context.Context, opts ...func(*CreateSvConfig)) (*sec
Description: "desc1", Description: "desc1",
Value: secretv0alpha1.NewExposedSecureValue("v1"), Value: secretv0alpha1.NewExposedSecureValue("v1"),
}, },
Status: secretv0alpha1.SecureValueStatus{ Status: secretv0alpha1.SecureValueStatus{},
Phase: secretv0alpha1.SecureValuePhasePending,
},
}, },
} }
for _, opt := range opts { for _, opt := range opts {

@ -1,44 +0,0 @@
package worker
import (
"github.com/prometheus/client_golang/prometheus"
)
const (
namespace = "grafana_secrets_manager"
subsystem = "outbox_worker"
)
// OutboxMetrics is a struct that contains all the metrics for an implementation of the secrets service.
type OutboxMetrics struct {
OutboxMessageProcessingDuration *prometheus.HistogramVec
}
func newOutboxMetrics() *OutboxMetrics {
return &OutboxMetrics{
OutboxMessageProcessingDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "message_processing_duration_seconds",
Help: "Duration of outbox message processing",
Buckets: prometheus.DefBuckets,
}, []string{"message_type", "keeper_type"}),
}
}
// NewOutboxMetrics creates a new SecretsMetrics struct containing registered metrics
func NewOutboxMetrics(reg prometheus.Registerer) *OutboxMetrics {
m := newOutboxMetrics()
if reg != nil {
reg.MustRegister(
m.OutboxMessageProcessingDuration,
)
}
return m
}
func NewTestMetrics() *OutboxMetrics {
return newOutboxMetrics()
}

@ -1,274 +0,0 @@
package worker
import (
"context"
"errors"
"fmt"
"time"
"github.com/grafana/grafana-app-sdk/logging"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/tracectx"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
// Consumes and processes messages from the secure value outbox queue
type Worker struct {
config Config
tracer trace.Tracer
database contracts.Database
outboxQueue contracts.OutboxQueue
secureValueMetadataStorage contracts.SecureValueMetadataStorage
keeperMetadataStorage contracts.KeeperMetadataStorage
keeperService contracts.KeeperService
encryptionManager contracts.EncryptionManager
metrics *OutboxMetrics
enabled bool
}
// DefaultConfig for the secure value outbox worker.
var DefaultConfig = Config{
BatchSize: 20,
ReceiveTimeout: 5 * time.Second,
PollingInterval: 100 * time.Millisecond,
MaxMessageProcessingAttempts: 10,
}
// ProvideWorkerConfig used for wire.
func ProvideWorkerConfig() Config {
return DefaultConfig
}
type Config struct {
// The max number of messages to fetch from the outbox queue in a batch
BatchSize uint
// How long to wait for a request to fetch messages from the outbox queue
ReceiveTimeout time.Duration
// How often to poll the outbox queue for new messages
PollingInterval time.Duration
// How many tries to try to process a message before marking the operation as failed
MaxMessageProcessingAttempts uint
}
func NewWorker(
config Config,
tracer trace.Tracer,
database contracts.Database,
outboxQueue contracts.OutboxQueue,
secureValueMetadataStorage contracts.SecureValueMetadataStorage,
keeperMetadataStorage contracts.KeeperMetadataStorage,
keeperService contracts.KeeperService,
encryptionManager contracts.EncryptionManager,
features featuremgmt.FeatureToggles,
reg prometheus.Registerer,
) (*Worker, error) {
if config.BatchSize == 0 {
return nil, fmt.Errorf("config.BatchSize is required")
}
if config.ReceiveTimeout == 0 {
return nil, fmt.Errorf("config.ReceiveTimeout is required")
}
if config.PollingInterval == 0 {
return nil, fmt.Errorf("config.PollingInterval is required")
}
if config.MaxMessageProcessingAttempts == 0 {
return nil, fmt.Errorf("config.MaxMessageProcessingAttempts is required")
}
// Require both features to be enabled for the worker to run.
enabled := features.IsEnabledGlobally(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs) && features.IsEnabledGlobally(featuremgmt.FlagSecretsManagementAppPlatform)
return &Worker{
config: config,
tracer: tracer,
database: database,
outboxQueue: outboxQueue,
secureValueMetadataStorage: secureValueMetadataStorage,
keeperMetadataStorage: keeperMetadataStorage,
keeperService: keeperService,
encryptionManager: encryptionManager,
metrics: NewOutboxMetrics(reg),
enabled: enabled,
}, nil
}
// Ensure that Worker implements the BackgroundService interface, so we can start it as a background service.
var _ registry.BackgroundService = (*Worker)(nil)
// Run is the main method to drive the worker
func (w *Worker) Run(ctx context.Context) error {
if !w.enabled {
return nil
}
logging.FromContext(ctx).Debug("starting worker control loop")
t := time.NewTicker(w.config.PollingInterval)
defer t.Stop()
for {
select {
// If the context was canceled
case <-ctx.Done():
// return the reason it was canceled
return ctx.Err()
// Otherwise try to receive messages
case <-t.C:
if ctx.Err() != nil {
return ctx.Err()
}
if err := w.ReceiveAndProcessMessages(ctx); err != nil {
logging.FromContext(ctx).Error("receiving outbox messages", "err", err.Error())
}
}
}
}
// TODO: don't rollback every message when a single error happens
func (w *Worker) ReceiveAndProcessMessages(ctx context.Context) error {
messageIDs := make([]int64, 0)
txErr := w.database.Transaction(ctx, func(ctx context.Context) error {
timeoutCtx, cancel := context.WithTimeout(ctx, w.config.ReceiveTimeout)
messages, err := w.outboxQueue.ReceiveN(timeoutCtx, w.config.BatchSize)
cancel()
if err != nil {
return err
}
for _, message := range messages {
messageIDs = append(messageIDs, message.MessageID)
if err := w.processMessage(ctx, message); err != nil {
return fmt.Errorf("processing message: %+v %w", message, err)
}
}
return nil
})
// This call is made outside the transaction to make sure the receive count is updated on rollbacks.
incrementErr := w.outboxQueue.IncrementReceiveCount(ctx, messageIDs)
if incrementErr != nil {
incrementErr = fmt.Errorf("incrementing receive count for outbox message: %w", incrementErr)
}
return errors.Join(txErr, incrementErr)
}
func (w *Worker) processMessage(ctx context.Context, message contracts.OutboxMessage) error {
start := time.Now()
keeperType := "unknown"
defer func() {
w.metrics.OutboxMessageProcessingDuration.WithLabelValues(string(message.Type), keeperType).Observe(time.Since(start).Seconds())
}()
logging.FromContext(ctx).Debug("processing message", "type", message.Type, "name", message.Name, "namespace", message.Namespace, "receiveCount", message.ReceiveCount)
opts := []trace.SpanStartOption{}
// If there's no request ID in the message, start a new root span and log an error.
ctx, err := tracectx.HexDecodeTraceIntoContext(ctx, message.RequestID)
if err != nil {
opts = append(opts, trace.WithNewRoot())
logging.FromContext(ctx).Error("decoding trace context from message", "err", err.Error(), "message.requestID", message.RequestID)
}
opts = append(opts, trace.WithAttributes(
attribute.String("message.requestID", message.RequestID),
attribute.Int64("message.id", message.MessageID),
attribute.String("message.type", string(message.Type)),
attribute.String("message.namespace", message.Namespace),
attribute.String("message.secureValue.name", message.Name),
attribute.Int("message.receive.count", message.ReceiveCount),
))
ctx, span := w.tracer.Start(ctx, "Worker.ProcessMessage", opts...)
defer span.End()
if message.ReceiveCount >= int(w.config.MaxMessageProcessingAttempts) {
if err := w.secureValueMetadataStorage.SetStatus(ctx, xkube.Namespace(message.Namespace), message.Name, secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseFailed, Message: fmt.Sprintf("Reached max number of attempts to complete operation: %s", message.Type)}); err != nil {
return fmt.Errorf("setting secret metadata status to Succeeded: message=%+v", message)
}
if err := w.outboxQueue.Delete(ctx, message.MessageID); err != nil {
return fmt.Errorf("deleting message from outbox queue: %w", err)
}
return nil
}
keeperCfg, err := w.keeperMetadataStorage.GetKeeperConfig(ctx, message.Namespace, message.KeeperName, contracts.ReadOpts{ForUpdate: true})
if err != nil {
return fmt.Errorf("fetching keeper config: namespace=%+v keeperName=%+v %w", message.Namespace, message.KeeperName, err)
}
keeperType = string(keeperCfg.Type())
keeper, err := w.keeperService.KeeperForConfig(keeperCfg)
if err != nil {
return fmt.Errorf("getting keeper for config: namespace=%+v keeperName=%+v %w", message.Namespace, message.KeeperName, err)
}
logging.FromContext(ctx).Debug("retrieved keeper", "namespace", message.Namespace, "keeperName", message.KeeperName, "type", keeperCfg.Type())
switch message.Type {
case contracts.CreateSecretOutboxMessage:
rawSecret, err := w.encryptionManager.Decrypt(ctx, message.Namespace, []byte(message.EncryptedSecret))
if err != nil {
return fmt.Errorf("decrypting secure value secret: %w", err)
}
externalID, err := keeper.Store(ctx, keeperCfg, message.Namespace, string(rawSecret))
if err != nil {
return fmt.Errorf("storing secret: message=%+v %w", message, err)
}
if err := w.secureValueMetadataStorage.SetExternalID(ctx, xkube.Namespace(message.Namespace), message.Name, externalID); err != nil {
return fmt.Errorf("setting secret metadata externalID: externalID=%+v message=%+v %w", externalID, message, err)
}
// Setting the status to Succeeded must be the last action
// since it acts as a fence to clients.
if err := w.secureValueMetadataStorage.SetStatus(ctx, xkube.Namespace(message.Namespace), message.Name, secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseSucceeded}); err != nil {
return fmt.Errorf("setting secret metadata status to Succeeded: message=%+v %w", message, err)
}
case contracts.UpdateSecretOutboxMessage:
rawSecret, err := w.encryptionManager.Decrypt(ctx, message.Namespace, []byte(message.EncryptedSecret))
if err != nil {
return fmt.Errorf("decrypting secure value secret: %w", err)
}
if err := keeper.Update(ctx, keeperCfg, message.Namespace, contracts.ExternalID(*message.ExternalID), string(rawSecret)); err != nil {
return fmt.Errorf("calling keeper to update secret: %w", err)
}
// Setting the status to Succeeded must be the last action
// since it acts as a fence to clients.
if err := w.secureValueMetadataStorage.SetStatus(ctx, xkube.Namespace(message.Namespace), message.Name, secretv0alpha1.SecureValueStatus{Phase: secretv0alpha1.SecureValuePhaseSucceeded}); err != nil {
return fmt.Errorf("setting secret metadata status to Succeeded: message=%+v", message)
}
case contracts.DeleteSecretOutboxMessage:
if err := keeper.Delete(ctx, keeperCfg, message.Namespace, contracts.ExternalID(*message.ExternalID)); err != nil {
return fmt.Errorf("calling keeper to delete secret: %w", err)
}
if err := w.secureValueMetadataStorage.Delete(ctx, xkube.Namespace(message.Namespace), message.Name); err != nil {
return fmt.Errorf("deleting secure value metadata: %+w", err)
}
default:
return fmt.Errorf("unhandled message type: %s", message.Type)
}
// Delete the message from the queue after completing all operations because
// if the message is deleted first, the response may be lost,
// resulting in an error, but since the message was actually deleted
// the worker would never retry.
if err := w.outboxQueue.Delete(ctx, message.MessageID); err != nil {
return fmt.Errorf("deleting message from outbox queue: %w", err)
}
return nil
}

@ -1,246 +0,0 @@
package worker_test
import (
"context"
"fmt"
"testing"
"time"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/testutils"
"github.com/grafana/grafana/pkg/registry/apis/secret/worker"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/stretchr/testify/require"
)
type fakeKeeperService struct {
keeperForConfigFunc func(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error)
}
func newFakeKeeperService(keeperForConfigFunc func(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error)) *fakeKeeperService {
return &fakeKeeperService{keeperForConfigFunc: keeperForConfigFunc}
}
func (s *fakeKeeperService) KeeperForConfig(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error) {
return s.keeperForConfigFunc(cfg)
}
func TestProcessMessage(t *testing.T) {
t.Parallel()
t.Run("secure value metadata status is set to Failed when processing a message fails too many times", func(t *testing.T) {
t.Parallel()
// Given a worker that will attempt to process a message N times
workerCfg := worker.Config{
BatchSize: 10,
ReceiveTimeout: 1 * time.Second,
PollingInterval: time.Millisecond,
MaxMessageProcessingAttempts: 2,
}
// And an error that keeps happening
keeperService := newFakeKeeperService(func(cfg secretv0alpha1.KeeperConfig) (contracts.Keeper, error) {
return nil, fmt.Errorf("oops")
})
sut := testutils.Setup(t, testutils.WithWorkerConfig(workerCfg), testutils.WithKeeperService(keeperService))
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
for range workerCfg.MaxMessageProcessingAttempts + 1 {
// The secure value status should be Pending while the worker is trying to process the message
sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, sv.Status.Phase)
// Worker tries to process messages
_ = sut.Worker.ReceiveAndProcessMessages(ctx)
}
// After the worker fails to process a message too many times,
// the secure value status is changed to Failed
sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhaseFailed, sv.Status.Phase)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Empty(t, messages)
})
t.Run("create sv: secure value metadata status is set to Succeeded when message is processed successfully", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
// Worker receives and processes the message
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
// and sets the secure value status to Succeeded
sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, sv.Status.Phase)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Empty(t, messages)
})
t.Run("update sv: secure value metadata status is set to Succeeded when message is processed successfully", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
// Worker receives and processes the message
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
// and sets the secure value status to Succeeded
sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, sv.Status.Phase)
sv.Spec.Description = "desc2"
sv.Spec.Value = secretv0alpha1.NewExposedSecureValue("v2")
// Queue an update operation
sv, err = sut.UpdateSv(ctx, sv)
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, sv.Status.Phase)
// Worker receives and processes the message
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
updatedSv, err := sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, updatedSv.Status.Phase)
require.Equal(t, sv.Spec.Description, updatedSv.Spec.Description)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Empty(t, messages)
})
t.Run("delete sv: secure value metadata is deleted", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
// Worker receives and processes the message
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
// and sets the secure value status to Succeeded
sv, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhaseSucceeded, sv.Status.Phase)
// Queue a delete operation
updatedSv, err := sut.DeleteSv(ctx, sv.Namespace, sv.Name)
require.NoError(t, err)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, updatedSv.Status.Phase)
// Worker receives and processes the message
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
// The secure value has been deleted
_, err = sut.SecureValueMetadataStorage.Read(ctx, xkube.Namespace(sv.Namespace), sv.Name, contracts.ReadOpts{})
require.ErrorIs(t, err, contracts.ErrSecureValueNotFound)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Empty(t, messages)
})
t.Run("when creating a secure value, the secret is encrypted before it is added to the outbox queue", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
var secret string
_, err := sut.CreateSv(ctx, func(cfg *testutils.CreateSvConfig) {
secret = string(cfg.Sv.Spec.Value)
})
require.NoError(t, err)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Equal(t, 1, len(messages))
encryptedSecret := messages[0].EncryptedSecret
require.NotEmpty(t, secret)
require.NotEmpty(t, encryptedSecret)
require.NotEqual(t, secret, encryptedSecret)
})
t.Run("when updating a secure value, the secret is encrypted before it is added to the outbox queue", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
sv.Spec.Value = secretv0alpha1.NewExposedSecureValue("v2")
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
newValue := "v2"
sv.Spec.Value = secretv0alpha1.NewExposedSecureValue(newValue)
// Queue an update secure value operation
_, err = sut.UpdateSv(ctx, sv)
require.NoError(t, err)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Equal(t, 1, len(messages))
encryptedSecret := messages[0].EncryptedSecret
require.NotEmpty(t, encryptedSecret)
require.NotEqual(t, newValue, encryptedSecret)
})
t.Run("when deleting a secure value, no value is added to the outbox message", func(t *testing.T) {
t.Parallel()
sut := testutils.Setup(t)
ctx := context.Background()
// Queue a create secure value operation
sv, err := sut.CreateSv(ctx)
require.NoError(t, err)
sv.Spec.Value = secretv0alpha1.NewExposedSecureValue("v2")
require.NoError(t, sut.Worker.ReceiveAndProcessMessages(ctx))
// Queue a delete secure value operation
_, err = sut.DeleteSv(ctx, sv.Namespace, sv.Name)
require.NoError(t, err)
messages, err := sut.OutboxQueue.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Equal(t, 1, len(messages))
require.Empty(t, messages[0].EncryptedSecret)
})
}

@ -9,7 +9,6 @@ import (
"github.com/grafana/grafana/pkg/infra/usagestats/statscollector" "github.com/grafana/grafana/pkg/infra/usagestats/statscollector"
"github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/registry"
apiregistry "github.com/grafana/grafana/pkg/registry/apis" apiregistry "github.com/grafana/grafana/pkg/registry/apis"
secretworker "github.com/grafana/grafana/pkg/registry/apis/secret/worker"
appregistry "github.com/grafana/grafana/pkg/registry/apps" appregistry "github.com/grafana/grafana/pkg/registry/apps"
"github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite" "github.com/grafana/grafana/pkg/services/accesscontrol/dualwrite"
"github.com/grafana/grafana/pkg/services/anonymous/anonimpl" "github.com/grafana/grafana/pkg/services/anonymous/anonimpl"
@ -71,7 +70,6 @@ func ProvideBackgroundServiceRegistry(
appRegistry *appregistry.Service, appRegistry *appregistry.Service,
pluginDashboardUpdater *plugindashboardsservice.DashboardUpdater, pluginDashboardUpdater *plugindashboardsservice.DashboardUpdater,
dashboardServiceImpl *service.DashboardServiceImpl, dashboardServiceImpl *service.DashboardServiceImpl,
secretManagerWorker *secretworker.Worker,
// Need to make sure these are initialized, is there a better place to put them? // Need to make sure these are initialized, is there a better place to put them?
_ dashboardsnapshots.Service, _ dashboardsnapshots.Service,
_ serviceaccounts.Service, _ serviceaccounts.Service,
@ -119,7 +117,6 @@ func ProvideBackgroundServiceRegistry(
appRegistry, appRegistry,
pluginDashboardUpdater, pluginDashboardUpdater,
dashboardServiceImpl, dashboardServiceImpl,
secretManagerWorker,
) )
} }

@ -46,7 +46,6 @@ import (
gsmEncryption "github.com/grafana/grafana/pkg/registry/apis/secret/encryption" gsmEncryption "github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
encryptionManager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager" encryptionManager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
secretsecurevalueservice "github.com/grafana/grafana/pkg/registry/apis/secret/service" secretsecurevalueservice "github.com/grafana/grafana/pkg/registry/apis/secret/service"
secretworker "github.com/grafana/grafana/pkg/registry/apis/secret/worker"
appregistry "github.com/grafana/grafana/pkg/registry/apps" appregistry "github.com/grafana/grafana/pkg/registry/apps"
"github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
@ -431,15 +430,12 @@ var wireBasicSet = wire.NewSet(
secretdecrypt.ProvideDecryptAllowList, secretdecrypt.ProvideDecryptAllowList,
secretencryption.ProvideDataKeyStorage, secretencryption.ProvideDataKeyStorage,
secretencryption.ProvideEncryptedValueStorage, secretencryption.ProvideEncryptedValueStorage,
secretmetadata.ProvideOutboxQueue,
secretsecurevalueservice.ProvideSecureValueService, secretsecurevalueservice.ProvideSecureValueService,
secretmigrator.NewWithEngine, secretmigrator.NewWithEngine,
secretdatabase.ProvideDatabase, secretdatabase.ProvideDatabase,
wire.Bind(new(secretcontracts.Database), new(*secretdatabase.Database)), wire.Bind(new(secretcontracts.Database), new(*secretdatabase.Database)),
encryptionManager.ProvideEncryptionManager, encryptionManager.ProvideEncryptionManager,
gsmEncryption.ProvideThirdPartyProviderMap, gsmEncryption.ProvideThirdPartyProviderMap,
secretworker.ProvideWorkerConfig,
secretworker.NewWorker,
// Unified storage // Unified storage
resource.ProvideStorageMetrics, resource.ProvideStorageMetrics,
resource.ProvideIndexMetrics, resource.ProvideIndexMetrics,

File diff suppressed because one or more lines are too long

@ -8,10 +8,8 @@ INSERT INTO {{ .Ident "secret_secure_value" }} (
{{ .Ident "created_by" }}, {{ .Ident "created_by" }},
{{ .Ident "updated" }}, {{ .Ident "updated" }},
{{ .Ident "updated_by" }}, {{ .Ident "updated_by" }},
{{ .Ident "status_phase" }}, {{ .Ident "active" }},
{{ if .Row.Message.Valid }} {{ .Ident "version" }},
{{ .Ident "status_message" }},
{{ end }}
{{ .Ident "description" }}, {{ .Ident "description" }},
{{ if .Row.Keeper.Valid }} {{ if .Row.Keeper.Valid }}
{{ .Ident "keeper" }}, {{ .Ident "keeper" }},
@ -33,10 +31,8 @@ INSERT INTO {{ .Ident "secret_secure_value" }} (
{{ .Arg .Row.CreatedBy }}, {{ .Arg .Row.CreatedBy }},
{{ .Arg .Row.Updated }}, {{ .Arg .Row.Updated }},
{{ .Arg .Row.UpdatedBy }}, {{ .Arg .Row.UpdatedBy }},
{{ .Arg .Row.Phase }}, {{ .Arg .Row.Active }},
{{ if .Row.Message.Valid }} {{ .Arg .Row.Version }},
{{ .Arg .Row.Message.String }},
{{ end }}
{{ .Arg .Row.Description }}, {{ .Arg .Row.Description }},
{{ if .Row.Keeper.Valid }} {{ if .Row.Keeper.Valid }}
{{ .Arg .Row.Keeper.String }}, {{ .Arg .Row.Keeper.String }},
@ -48,4 +44,4 @@ INSERT INTO {{ .Ident "secret_secure_value" }} (
{{ .Arg .Row.Ref.String }}, {{ .Arg .Row.Ref.String }},
{{ end }} {{ end }}
{{ .Arg .Row.ExternalID }} {{ .Arg .Row.ExternalID }}
); );

@ -1,4 +0,0 @@
DELETE FROM {{ .Ident "secret_secure_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }}
;

@ -0,0 +1,10 @@
SELECT
{{ .Ident "version" }}
FROM
{{ .Ident "secret_secure_value" }}
WHERE
{{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }}
ORDER BY {{ .Ident "version" }} DESC
LIMIT 1
;

@ -8,15 +8,17 @@ SELECT
{{ .Ident "created_by" }}, {{ .Ident "created_by" }},
{{ .Ident "updated" }}, {{ .Ident "updated" }},
{{ .Ident "updated_by" }}, {{ .Ident "updated_by" }},
{{ .Ident "status_phase" }},
{{ .Ident "status_message" }},
{{ .Ident "description" }}, {{ .Ident "description" }},
{{ .Ident "keeper" }}, {{ .Ident "keeper" }},
{{ .Ident "decrypters" }}, {{ .Ident "decrypters" }},
{{ .Ident "ref" }}, {{ .Ident "ref" }},
{{ .Ident "external_id" }} {{ .Ident "external_id" }},
{{ .Ident "version" }},
{{ .Ident "active" }}
FROM FROM
{{ .Ident "secret_secure_value" }} {{ .Ident "secret_secure_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} WHERE
{{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "active" }} = true
ORDER BY {{ .Ident "updated" }} DESC ORDER BY {{ .Ident "updated" }} DESC
; ;

@ -5,7 +5,9 @@ SELECT
{{ .Ident "keeper" }} {{ .Ident "keeper" }}
FROM FROM
{{ .Ident "secret_secure_value" }} {{ .Ident "secret_secure_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND WHERE
{{ .Ident "name" }} IN ({{ .ArgList .UsedSecureValues }}) {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} IN ({{ .ArgList .UsedSecureValues }}) AND
{{ .Ident "active" }} = true
{{ .SelectFor "UPDATE" }} {{ .SelectFor "UPDATE" }}
; ;

@ -1,33 +0,0 @@
INSERT INTO {{ .Ident "secret_secure_value_outbox" }} (
{{ .Ident "request_id" }},
{{ .Ident "message_type" }},
{{ .Ident "name" }},
{{ .Ident "namespace" }},
{{ if .Row.EncryptedSecret.Valid }}
{{ .Ident "encrypted_secret" }},
{{ end }}
{{ if .Row.KeeperName.Valid }}
{{ .Ident "keeper_name" }},
{{ end }}
{{ if .Row.ExternalID.Valid }}
{{ .Ident "external_id" }},
{{ end }}
{{ .Ident "receive_count" }},
{{ .Ident "created" }}
) VALUES (
{{ .Arg .Row.RequestID }},
{{ .Arg .Row.MessageType }},
{{ .Arg .Row.Name }},
{{ .Arg .Row.Namespace }},
{{ if .Row.EncryptedSecret.Valid }}
{{ .Arg .Row.EncryptedSecret.String }},
{{ end }}
{{ if .Row.KeeperName.Valid }}
{{ .Arg .Row.KeeperName.String }},
{{ end }}
{{ if .Row.ExternalID.Valid }}
{{ .Arg .Row.ExternalID.String }},
{{ end }}
{{ .Arg .Row.ReceiveCount }},
{{ .Arg .Row.Created }}
);

@ -1,5 +0,0 @@
DELETE FROM
{{ .Ident "secret_secure_value_outbox" }}
WHERE
{{ .Ident "id" }} = {{ .Arg .MessageID }}
;

@ -1,6 +0,0 @@
SELECT
{{ .Ident "id" }}
FROM {{ .Ident "secret_secure_value_outbox" }}
ORDER BY id ASC
LIMIT {{ .Arg .ReceiveLimit }}
;

@ -1,8 +0,0 @@
SELECT
{{ .Ident "created" }},
{{ .Ident "message_type" }}
FROM
{{ .Ident "secret_secure_value_outbox" }}
WHERE
{{ .Ident "id" }} = {{ .Arg .MessageID }}
;

@ -1,19 +0,0 @@
SELECT
{{ .Ident "request_id" }},
{{ .Ident "id" }},
{{ .Ident "message_type" }},
{{ .Ident "name" }},
{{ .Ident "namespace" }},
{{ .Ident "encrypted_secret" }},
{{ .Ident "keeper_name" }},
{{ .Ident "external_id" }},
{{ .Ident "receive_count" }},
{{ .Ident "created" }}
FROM
{{ .Ident "secret_secure_value_outbox" }}
WHERE
{{ .Ident "id" }} IN ({{ .ArgList .MessageIDs }})
ORDER BY
{{ .Ident "id" }} ASC
{{ .SelectFor "UPDATE SKIP LOCKED" }}
;

@ -1,7 +0,0 @@
UPDATE
{{ .Ident "secret_secure_value_outbox" }}
SET
{{ .Ident "receive_count" }} = {{ .Ident "receive_count" }} + 1
WHERE
{{ .Ident "id" }} IN ({{ .ArgList .MessageIDs }})
;

@ -8,18 +8,20 @@ SELECT
{{ .Ident "created_by" }}, {{ .Ident "created_by" }},
{{ .Ident "updated" }}, {{ .Ident "updated" }},
{{ .Ident "updated_by" }}, {{ .Ident "updated_by" }},
{{ .Ident "status_phase" }},
{{ .Ident "status_message" }},
{{ .Ident "description" }}, {{ .Ident "description" }},
{{ .Ident "keeper" }}, {{ .Ident "keeper" }},
{{ .Ident "decrypters" }}, {{ .Ident "decrypters" }},
{{ .Ident "ref" }}, {{ .Ident "ref" }},
{{ .Ident "external_id" }} {{ .Ident "external_id" }},
{{ .Ident "active" }},
{{ .Ident "version" }}
FROM FROM
{{ .Ident "secret_secure_value" }} {{ .Ident "secret_secure_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND WHERE
{{ .Ident "name" }} = {{ .Arg .Name }} {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }} AND
{{ .Ident "active" }} = true
{{ if .IsForUpdate }} {{ if .IsForUpdate }}
{{ .SelectFor "UPDATE" }} {{ .SelectFor "UPDATE" }}
{{ end }} {{ end }}
; ;

@ -2,9 +2,12 @@ SELECT
{{ .Ident "keeper" }}, {{ .Ident "keeper" }},
{{ .Ident "decrypters" }}, {{ .Ident "decrypters" }},
{{ .Ident "ref" }}, {{ .Ident "ref" }},
{{ .Ident "external_id" }} {{ .Ident "external_id" }},
{{ .Ident "active" }}
FROM FROM
{{ .Ident "secret_secure_value" }} {{ .Ident "secret_secure_value" }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND WHERE
{{ .Ident "name" }} = {{ .Arg .Name }} {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
; {{ .Ident "name" }} = {{ .Arg .Name }} AND
{{ .Ident "active" }} = true
;

@ -0,0 +1,8 @@
UPDATE
{{ .Ident "secret_secure_value" }}
SET
{{ .Ident "active" }} = ({{ .Ident "version" }} = {{ .Arg .Version}})
WHERE
{{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }}
;

@ -0,0 +1,9 @@
UPDATE
{{ .Ident "secret_secure_value" }}
SET
{{ .Ident "active" }} = false
WHERE
{{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }} AND
{{ .Ident "version" }} = {{ .Arg .Version }}
;

@ -1,30 +0,0 @@
UPDATE
{{ .Ident "secret_secure_value" }}
SET
{{ .Ident "guid" }} = {{ .Arg .Row.GUID }},
{{ .Ident "name" }} = {{ .Arg .Row.Name }},
{{ .Ident "namespace" }} = {{ .Arg .Row.Namespace }},
{{ .Ident "annotations" }} = {{ .Arg .Row.Annotations }},
{{ .Ident "labels" }} = {{ .Arg .Row.Labels }},
{{ .Ident "created" }} = {{ .Arg .Row.Created }},
{{ .Ident "created_by" }} = {{ .Arg .Row.CreatedBy }},
{{ .Ident "updated" }} = {{ .Arg .Row.Updated }},
{{ .Ident "updated_by" }} = {{ .Arg .Row.UpdatedBy }},
{{ .Ident "status_phase" }} = {{ .Arg .Row.Phase }},
{{ if .Row.Message.Valid }}
{{ .Ident "status_message" }} = {{ .Arg .Row.Message.String }},
{{ end }}
{{ .Ident "description" }} = {{ .Arg .Row.Description }},
{{ if .Row.Keeper.Valid }}
{{ .Ident "keeper" }} = {{ .Arg .Row.Keeper.String }},
{{ end }}
{{ if .Row.Decrypters.Valid }}
{{ .Ident "decrypters" }} = {{ .Arg .Row.Decrypters.String }},
{{ end }}
{{ if .Row.Ref.Valid }}
{{ .Ident "ref" }} = {{ .Arg .Row.Ref.String }},
{{ end }}
{{ .Ident "external_id" }} = {{ .Arg .Row.ExternalID }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Row.Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Row.Name }}
;

@ -2,6 +2,8 @@ UPDATE
{{ .Ident "secret_secure_value" }} {{ .Ident "secret_secure_value" }}
SET SET
{{ .Ident "external_id" }} = {{ .Arg .ExternalID }} {{ .Ident "external_id" }} = {{ .Arg .ExternalID }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND WHERE
{{ .Ident "name" }} = {{ .Arg .Name }} {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
; {{ .Ident "name" }} = {{ .Arg .Name }} AND
{{ .Ident "version" }} = {{ .Arg .Version }}
;

@ -1,8 +0,0 @@
UPDATE
{{ .Ident "secret_secure_value" }}
SET
{{ .Ident "status_phase" }} = {{ .Arg .Phase }},
{{ .Ident "status_message" }} = {{ .Arg .Message }}
WHERE {{ .Ident "namespace" }} = {{ .Arg .Namespace }} AND
{{ .Ident "name" }} = {{ .Arg .Name }}
;

@ -1,4 +1,4 @@
package metadata package metadata_test
import ( import (
"context" "context"
@ -7,23 +7,11 @@ import (
"github.com/grafana/authlib/authn" "github.com/grafana/authlib/authn"
"github.com/grafana/authlib/types" "github.com/grafana/authlib/types"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
"github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/apimachinery/identity"
secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1" secretv0alpha1 "github.com/grafana/grafana/pkg/apis/secret/v0alpha1"
"github.com/grafana/grafana/pkg/infra/usagestats"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts" "github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/registry/apis/secret/decrypt" "github.com/grafana/grafana/pkg/registry/apis/secret/testutils"
"github.com/grafana/grafana/pkg/registry/apis/secret/encryption"
encryptionmanager "github.com/grafana/grafana/pkg/registry/apis/secret/encryption/manager"
"github.com/grafana/grafana/pkg/registry/apis/secret/secretkeeper"
"github.com/grafana/grafana/pkg/registry/apis/secret/xkube"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/storage/secret/database"
encryptionstorage "github.com/grafana/grafana/pkg/storage/secret/encryption"
"github.com/grafana/grafana/pkg/storage/secret/migrator"
) )
func TestIntegrationDecrypt(t *testing.T) { func TestIntegrationDecrypt(t *testing.T) {
@ -39,9 +27,9 @@ func TestIntegrationDecrypt(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
t.Cleanup(cancel) t.Cleanup(cancel)
decryptSvc, _, _, _ := setupDecryptTestService(t, nil) sut := testutils.Setup(t)
exposed, err := decryptSvc.Decrypt(ctx, "default", "name") exposed, err := sut.DecryptStorage.Decrypt(ctx, "default", "name")
require.Error(t, err) require.Error(t, err)
require.Empty(t, exposed) require.Empty(t, exposed)
}) })
@ -55,9 +43,11 @@ func TestIntegrationDecrypt(t *testing.T) {
// Create auth context with proper permissions // Create auth context with proper permissions
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/group1:decrypt"}, "svc", types.TypeUser) authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/group1:decrypt"}, "svc", types.TypeUser)
decryptSvc, _, _, _ := setupDecryptTestService(t, map[string]struct{}{"group1": {}}) sut := testutils.Setup(t, testutils.WithMutateCfg(func(sc *testutils.SetupConfig) {
sc.AllowList = map[string]struct{}{"group1": {}}
}))
exposed, err := decryptSvc.Decrypt(authCtx, "default", "non-existent-value") exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "non-existent-value")
require.ErrorIs(t, err, contracts.ErrDecryptNotFound) require.ErrorIs(t, err, contracts.ErrDecryptNotFound)
require.Empty(t, exposed) require.Empty(t, exposed)
}) })
@ -78,7 +68,9 @@ func TestIntegrationDecrypt(t *testing.T) {
allowList := map[string]struct{}{"allowed-group": {}} allowList := map[string]struct{}{"allowed-group": {}}
// Setup service // Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, allowList) sut := testutils.Setup(t, testutils.WithMutateCfg(func(sc *testutils.SetupConfig) {
sc.AllowList = allowList
}))
// Create a secure value that is not in the allowlist // Create a secure value that is not in the allowlist
spec := secretv0alpha1.SecureValueSpec{ spec := secretv0alpha1.SecureValueSpec{
@ -90,9 +82,10 @@ func TestIntegrationDecrypt(t *testing.T) {
sv.Name = svName sv.Name = svName
sv.Namespace = "default" sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv))
require.NoError(t, err)
exposed, err := decryptSvc.Decrypt(authCtx, "default", svName) exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", svName)
require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized)
require.Empty(t, exposed) require.Empty(t, exposed)
}) })
@ -112,7 +105,9 @@ func TestIntegrationDecrypt(t *testing.T) {
allowList := map[string]struct{}{svcIdentity: {}} allowList := map[string]struct{}{svcIdentity: {}}
// Setup service // Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, allowList) sut := testutils.Setup(t, testutils.WithMutateCfg(func(sc *testutils.SetupConfig) {
sc.AllowList = allowList
}))
// Create a secure value that is in the allowlist // Create a secure value that is in the allowlist
spec := secretv0alpha1.SecureValueSpec{ spec := secretv0alpha1.SecureValueSpec{
@ -124,9 +119,10 @@ func TestIntegrationDecrypt(t *testing.T) {
sv.Name = "sv-test" sv.Name = "sv-test"
sv.Namespace = "default" sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv))
require.NoError(t, err)
exposed, err := decryptSvc.Decrypt(authCtx, "default", "sv-test") exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "sv-test")
require.NoError(t, err) require.NoError(t, err)
require.NotEmpty(t, exposed) require.NotEmpty(t, exposed)
require.Equal(t, "value", exposed.DangerouslyExposeAndConsumeValue()) require.Equal(t, "value", exposed.DangerouslyExposeAndConsumeValue())
@ -148,7 +144,9 @@ func TestIntegrationDecrypt(t *testing.T) {
allowList := map[string]struct{}{svcIdentity: {}} allowList := map[string]struct{}{svcIdentity: {}}
// Setup service // Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, allowList) sut := testutils.Setup(t, testutils.WithMutateCfg(func(sc *testutils.SetupConfig) {
sc.AllowList = allowList
}))
// Create a secure value that is in the allowlist // Create a secure value that is in the allowlist
spec := secretv0alpha1.SecureValueSpec{ spec := secretv0alpha1.SecureValueSpec{
@ -160,9 +158,10 @@ func TestIntegrationDecrypt(t *testing.T) {
sv.Name = svName sv.Name = svName
sv.Namespace = "default" sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv))
require.NoError(t, err)
exposed, err := decryptSvc.Decrypt(authCtx, "default", svName) exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", svName)
require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized)
require.Empty(t, exposed) require.Empty(t, exposed)
}) })
@ -179,7 +178,7 @@ func TestIntegrationDecrypt(t *testing.T) {
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues"}, svcIdentity, types.TypeUser) authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues"}, svcIdentity, types.TypeUser)
// Setup service // Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}}) sut := testutils.Setup(t)
// Create a secure value // Create a secure value
spec := secretv0alpha1.SecureValueSpec{ spec := secretv0alpha1.SecureValueSpec{
@ -191,9 +190,10 @@ func TestIntegrationDecrypt(t *testing.T) {
sv.Name = "sv-test" sv.Name = "sv-test"
sv.Namespace = "default" sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv))
require.NoError(t, err)
exposed, err := decryptSvc.Decrypt(authCtx, "default", "sv-test") exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "sv-test")
require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized)
require.Empty(t, exposed) require.Empty(t, exposed)
}) })
@ -211,7 +211,7 @@ func TestIntegrationDecrypt(t *testing.T) {
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/" + svName + ":read"}, svcIdentity, types.TypeUser) authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/" + svName + ":read"}, svcIdentity, types.TypeUser)
// Setup service // Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}}) sut := testutils.Setup(t)
// Create a secure value // Create a secure value
spec := secretv0alpha1.SecureValueSpec{ spec := secretv0alpha1.SecureValueSpec{
@ -223,9 +223,10 @@ func TestIntegrationDecrypt(t *testing.T) {
sv.Name = svName sv.Name = svName
sv.Namespace = "default" sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv))
require.NoError(t, err)
exposed, err := decryptSvc.Decrypt(authCtx, "default", svName) exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", svName)
require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized)
require.Empty(t, exposed) require.Empty(t, exposed)
}) })
@ -242,7 +243,7 @@ func TestIntegrationDecrypt(t *testing.T) {
authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/:decrypt"}, svcIdentity, types.TypeUser) authCtx := createAuthContext(ctx, "default", []string{"secret.grafana.app/securevalues/:decrypt"}, svcIdentity, types.TypeUser)
// Setup service // Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}}) sut := testutils.Setup(t)
// Create a secure value // Create a secure value
spec := secretv0alpha1.SecureValueSpec{ spec := secretv0alpha1.SecureValueSpec{
@ -254,9 +255,10 @@ func TestIntegrationDecrypt(t *testing.T) {
sv.Name = "sv-test" sv.Name = "sv-test"
sv.Namespace = "default" sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv))
require.NoError(t, err)
exposed, err := decryptSvc.Decrypt(authCtx, "default", "sv-test") exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", "sv-test")
require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized) require.ErrorIs(t, err, contracts.ErrDecryptNotAuthorized)
require.Empty(t, exposed) require.Empty(t, exposed)
}) })
@ -274,7 +276,7 @@ func TestIntegrationDecrypt(t *testing.T) {
authCtx := createAuthContext(ctx, "default", []string{"wrong.group/securevalues/" + svName + ":decrypt"}, svcIdentity, types.TypeUser) authCtx := createAuthContext(ctx, "default", []string{"wrong.group/securevalues/" + svName + ":decrypt"}, svcIdentity, types.TypeUser)
// Setup service // Setup service
decryptSvc, secureValueMetadataStorage, keeperService, keeperMetadataService := setupDecryptTestService(t, map[string]struct{}{svcIdentity: {}}) sut := testutils.Setup(t)
// Create a secure value // Create a secure value
spec := secretv0alpha1.SecureValueSpec{ spec := secretv0alpha1.SecureValueSpec{
@ -286,9 +288,10 @@ func TestIntegrationDecrypt(t *testing.T) {
sv.Name = svName sv.Name = svName
sv.Namespace = "default" sv.Namespace = "default"
newTestSecureValue(authCtx, t, secureValueMetadataStorage, keeperService, keeperMetadataService, sv, "actor-uid") _, err := sut.CreateSv(authCtx, testutils.CreateSvWithSv(sv))
require.NoError(t, err)
exposed, err := decryptSvc.Decrypt(authCtx, "default", svName) exposed, err := sut.DecryptStorage.Decrypt(authCtx, "default", svName)
require.Error(t, err) require.Error(t, err)
require.Equal(t, err.Error(), "not authorized") require.Equal(t, err.Error(), "not authorized")
require.Empty(t, exposed) require.Empty(t, exposed)
@ -297,62 +300,6 @@ func TestIntegrationDecrypt(t *testing.T) {
// TODO: add more tests for keeper failure scenarios, lets see how the async work will change this though. // TODO: add more tests for keeper failure scenarios, lets see how the async work will change this though.
} }
func setupDecryptTestService(t *testing.T, allowList map[string]struct{}) (*decryptStorage, contracts.SecureValueMetadataStorage, *secretkeeper.OSSKeeperService, contracts.KeeperMetadataStorage) {
t.Helper()
// Initialize infra dependencies
cfg := &setting.Cfg{
SecretsManagement: setting.SecretsManagerSettings{
SecretKey: "sdDkslslld",
EncryptionProvider: "secretKey.v1",
},
}
features := featuremgmt.WithFeatures(
featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs,
featuremgmt.FlagSecretsManagementAppPlatform,
)
db := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
tracer := noop.NewTracerProvider().Tracer("test")
database := database.ProvideDatabase(db, tracer)
// Initialize encryption manager and storage
dataKeyStore, err := encryptionstorage.ProvideDataKeyStorage(database, tracer, features, nil)
require.NoError(t, err)
encValueStore, err := encryptionstorage.ProvideEncryptedValueStorage(database, tracer, features)
require.NoError(t, err)
encryptionManager, err := encryptionmanager.ProvideEncryptionManager(
tracer,
dataKeyStore,
cfg,
&usagestats.UsageStatsMock{},
encryption.ProviderMap{},
)
require.NoError(t, err)
// Initialize the keeper service
keeperService, err := secretkeeper.ProvideService(tracer, encValueStore, encryptionManager, nil)
require.NoError(t, err)
keeperMetadataStorage, err := ProvideKeeperMetadataStorage(database, tracer, features, nil)
require.NoError(t, err)
// Initialize the secure value storage
secureValueMetadataStorage, err := ProvideSecureValueMetadataStorage(database, tracer, features, nil)
require.NoError(t, err)
decryptAuthorizer := decrypt.ProvideDecryptAuthorizer(tracer, allowList)
// Initialize the decrypt storage
decryptSvc, err := ProvideDecryptStorage(features, tracer, keeperService, keeperMetadataStorage, secureValueMetadataStorage, decryptAuthorizer, nil)
require.NoError(t, err)
return decryptSvc.(*decryptStorage), secureValueMetadataStorage, keeperService, keeperMetadataStorage
}
func createAuthContext(ctx context.Context, namespace string, permissions []string, svc string, identityType types.IdentityType) context.Context { func createAuthContext(ctx context.Context, namespace string, permissions []string, svc string, identityType types.IdentityType) context.Context {
requester := &identity.StaticRequester{ requester := &identity.StaticRequester{
Type: identityType, Type: identityType,
@ -371,32 +318,3 @@ func createAuthContext(ctx context.Context, namespace string, permissions []stri
return types.WithAuthInfo(ctx, requester) return types.WithAuthInfo(ctx, requester)
} }
// This helper will also delete the secureValue from the db when the test is done.
func newTestSecureValue(ctx context.Context, t *testing.T, db contracts.SecureValueMetadataStorage, keeperService *secretkeeper.OSSKeeperService, keeperMetadataStorage contracts.KeeperMetadataStorage, sv *secretv0alpha1.SecureValue, actorUID string) {
t.Helper()
_, err := db.Create(ctx, sv, actorUID)
require.NoError(t, err)
require.NoError(t, err)
// Since creating secrets is async, store the secret in the keeper synchronously to make testing easier
cfg, err := keeperMetadataStorage.GetKeeperConfig(ctx, sv.Namespace, sv.Spec.Keeper, contracts.ReadOpts{})
require.NoError(t, err)
keeper, err := keeperService.KeeperForConfig(cfg)
require.NoError(t, err)
externalID, err := keeper.Store(ctx, cfg, sv.Namespace, sv.Spec.Value.DangerouslyExposeAndConsumeValue())
require.NoError(t, err)
// Set external id for the secure value
err = db.SetExternalID(ctx, xkube.Namespace(sv.Namespace), sv.Name, externalID)
require.NoError(t, err)
t.Cleanup(func() {
require.NoError(t, keeper.Delete(ctx, cfg, sv.Namespace, externalID))
require.NoError(t, db.Delete(ctx, xkube.Namespace(sv.Namespace), sv.Name))
})
}

@ -1,4 +1,4 @@
package metadata package metadata_test
import ( import (
"context" "context"
@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/storage/secret/database" "github.com/grafana/grafana/pkg/storage/secret/database"
"github.com/grafana/grafana/pkg/storage/secret/metadata"
"github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/grafana/grafana/pkg/storage/secret/migrator"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop" "go.opentelemetry.io/otel/trace/noop"
@ -340,7 +341,7 @@ func initStorage(t *testing.T) contracts.KeeperMetadataStorage {
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
// Initialize the keeper storage // Initialize the keeper storage
keeperMetadataStorage, err := ProvideKeeperMetadataStorage(db, tracer, features, nil) keeperMetadataStorage, err := metadata.ProvideKeeperMetadataStorage(db, tracer, features, nil)
require.NoError(t, err) require.NoError(t, err)
return keeperMetadataStorage return keeperMetadataStorage
} }

@ -13,16 +13,6 @@ const (
// StorageMetrics is a struct that contains all the metrics for all operations of secrets storage. // StorageMetrics is a struct that contains all the metrics for all operations of secrets storage.
type StorageMetrics struct { type StorageMetrics struct {
OutboxAppendDuration *prometheus.HistogramVec
OutboxReceiveDuration prometheus.Histogram
OutboxAppendCount *prometheus.CounterVec
OutboxReceiveCount prometheus.Counter
OutboxDeleteDuration prometheus.Histogram
OutboxDeleteCount prometheus.Counter
OutboxIncrementReceiveCountDuration prometheus.Histogram
OutboxIncrementReceiveCountCount prometheus.Counter
OutboxTotalMessageLifetimeDuration *prometheus.HistogramVec
KeeperMetadataCreateDuration *prometheus.HistogramVec KeeperMetadataCreateDuration *prometheus.HistogramVec
KeeperMetadataCreateCount *prometheus.CounterVec KeeperMetadataCreateCount *prometheus.CounterVec
KeeperMetadataUpdateDuration *prometheus.HistogramVec KeeperMetadataUpdateDuration *prometheus.HistogramVec
@ -55,67 +45,6 @@ type StorageMetrics struct {
func newStorageMetrics() *StorageMetrics { func newStorageMetrics() *StorageMetrics {
return &StorageMetrics{ return &StorageMetrics{
// Outbox metrics
OutboxAppendDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "outbox_append_duration_seconds",
Help: "Duration of outbox message append operations",
Buckets: prometheus.DefBuckets,
}, []string{"message_type"}),
OutboxAppendCount: prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "outbox_append_count",
Help: "Count of outbox message append operations",
}, []string{"message_type"}),
OutboxReceiveDuration: prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "outbox_receive_duration_seconds",
Help: "Duration of outbox message receive operations",
Buckets: prometheus.DefBuckets,
}),
OutboxReceiveCount: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "outbox_receive_count",
Help: "Count of outbox message receive operations",
}),
OutboxDeleteDuration: prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "outbox_delete_duration_seconds",
Help: "Duration of outbox message delete operations",
Buckets: prometheus.DefBuckets,
}),
OutboxDeleteCount: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "outbox_delete_count",
Help: "Count of outbox message delete operations",
}),
OutboxIncrementReceiveCountDuration: prometheus.NewHistogram(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "outbox_increment_receive_count_duration_seconds",
Help: "Duration of outbox message increment receive count operations",
Buckets: prometheus.DefBuckets,
}),
OutboxIncrementReceiveCountCount: prometheus.NewCounter(prometheus.CounterOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "outbox_increment_receive_count_count",
Help: "Count of outbox message increment receive count operations",
}),
OutboxTotalMessageLifetimeDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace,
Subsystem: subsystem,
Name: "outbox_total_message_lifetime_duration_seconds",
Help: "Total duration of outbox message lifetime",
Buckets: prometheus.DefBuckets,
}, []string{"message_type"}),
// Keeper metrics // Keeper metrics
KeeperMetadataCreateDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{ KeeperMetadataCreateDuration: prometheus.NewHistogramVec(prometheus.HistogramOpts{
Namespace: namespace, Namespace: namespace,
@ -307,15 +236,6 @@ func NewStorageMetrics(reg prometheus.Registerer) *StorageMetrics {
if reg != nil { if reg != nil {
reg.MustRegister( reg.MustRegister(
m.OutboxAppendDuration,
m.OutboxAppendCount,
m.OutboxReceiveDuration,
m.OutboxReceiveCount,
m.OutboxDeleteDuration,
m.OutboxDeleteCount,
m.OutboxIncrementReceiveCountDuration,
m.OutboxIncrementReceiveCountCount,
m.OutboxTotalMessageLifetimeDuration,
m.KeeperMetadataCreateDuration, m.KeeperMetadataCreateDuration,
m.KeeperMetadataCreateCount, m.KeeperMetadataCreateCount,
m.KeeperMetadataUpdateDuration, m.KeeperMetadataUpdateDuration,

@ -1,397 +0,0 @@
package metadata
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/grafana/grafana/pkg/storage/secret/metadata/metrics"
unifiedsql "github.com/grafana/grafana/pkg/storage/unified/sql"
"github.com/prometheus/client_golang/prometheus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"github.com/grafana/grafana/pkg/registry/apis/secret/assert"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/storage/unified/sql/sqltemplate"
)
type outboxStore struct {
db contracts.Database
dialect sqltemplate.Dialect
tracer trace.Tracer
metrics *metrics.StorageMetrics
}
func ProvideOutboxQueue(
db contracts.Database,
tracer trace.Tracer,
reg prometheus.Registerer,
) contracts.OutboxQueue {
return &outboxStore{
db: db,
dialect: sqltemplate.DialectForDriver(db.DriverName()),
metrics: metrics.NewStorageMetrics(reg),
tracer: tracer,
}
}
type outboxMessageDB struct {
RequestID string
MessageID int64
MessageType contracts.OutboxMessageType
Name string
Namespace string
EncryptedSecret sql.NullString
KeeperName sql.NullString
ExternalID sql.NullString
ReceiveCount int
Created int64
}
func (s *outboxStore) Append(ctx context.Context, input contracts.AppendOutboxMessage) (messageID int64, err error) {
ctx, span := s.tracer.Start(ctx, "outboxStore.Append", trace.WithAttributes(
attribute.String("name", input.Name),
attribute.String("namespace", input.Namespace),
attribute.String("type", string(input.Type)),
attribute.String("requestID", input.RequestID),
))
defer span.End()
defer func() {
if err != nil {
span.SetStatus(codes.Error, "failed to append outbox message")
span.RecordError(err)
}
if messageID != 0 {
span.SetAttributes(attribute.Int64("messageID", messageID))
}
}()
assert.True(input.Type != "", "outboxStore.Append: outbox message type is required")
start := time.Now()
messageID, err = s.insertMessage(ctx, input)
if err != nil {
return messageID, fmt.Errorf("inserting message into outbox table: %+w", err)
}
s.metrics.OutboxAppendDuration.WithLabelValues(string(input.Type)).Observe(time.Since(start).Seconds())
s.metrics.OutboxAppendCount.WithLabelValues(string(input.Type)).Inc()
return messageID, nil
}
func (s *outboxStore) insertMessage(ctx context.Context, input contracts.AppendOutboxMessage) (int64, error) {
keeperName := sql.NullString{}
if input.KeeperName != nil {
keeperName = sql.NullString{
Valid: true,
String: *input.KeeperName,
}
}
externalID := sql.NullString{}
if input.ExternalID != nil {
externalID = sql.NullString{
Valid: true,
String: *input.ExternalID,
}
}
encryptedSecret := sql.NullString{}
if input.Type == contracts.CreateSecretOutboxMessage || input.Type == contracts.UpdateSecretOutboxMessage {
encryptedSecret = sql.NullString{
Valid: true,
String: input.EncryptedSecret,
}
}
req := appendSecureValueOutbox{
SQLTemplate: sqltemplate.New(s.dialect),
Row: &outboxMessageDB{
RequestID: input.RequestID,
MessageType: input.Type,
Name: input.Name,
Namespace: input.Namespace,
EncryptedSecret: encryptedSecret,
KeeperName: keeperName,
ExternalID: externalID,
ReceiveCount: 0,
Created: time.Now().UTC().UnixMilli(),
},
}
query, err := sqltemplate.Execute(sqlSecureValueOutboxAppend, req)
if err != nil {
return 0, fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxAppend.Name(), err)
}
result, err := s.db.ExecContext(ctx, query, req.GetArgs()...)
if err != nil {
if unifiedsql.IsRowAlreadyExistsError(err) {
return 0, contracts.ErrSecureValueOperationInProgress
}
return 0, fmt.Errorf("inserting message into secure value outbox table: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return 0, fmt.Errorf("get rows affected: %w", err)
}
if rowsAffected != 1 {
return 0, fmt.Errorf("expected to affect 1 row, but affected %d", rowsAffected)
}
id, err := result.LastInsertId()
if err != nil {
return id, fmt.Errorf("fetching last inserted id: %w", err)
}
return id, nil
}
func (s *outboxStore) ReceiveN(ctx context.Context, limit uint) ([]contracts.OutboxMessage, error) {
messageIDs, err := s.fetchMessageIdsInQueue(ctx, limit)
if err != nil {
return nil, fmt.Errorf("fetching message ids from queue: %w", err)
}
// If queue is empty
if len(messageIDs) == 0 {
return nil, nil
}
req := receiveNSecureValueOutbox{
SQLTemplate: sqltemplate.New(s.dialect),
MessageIDs: messageIDs,
}
start := time.Now()
query, err := sqltemplate.Execute(sqlSecureValueOutboxReceiveN, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxReceiveN.Name(), err)
}
rows, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil {
return nil, fmt.Errorf("fetching rows from secure value outbox table: %w", err)
}
defer func() { _ = rows.Close() }()
messages := make([]contracts.OutboxMessage, 0)
for rows.Next() {
var row outboxMessageDB
if err := rows.Scan(
&row.RequestID,
&row.MessageID,
&row.MessageType,
&row.Name,
&row.Namespace,
&row.EncryptedSecret,
&row.KeeperName,
&row.ExternalID,
&row.ReceiveCount,
&row.Created,
); err != nil {
return nil, fmt.Errorf("scanning row from secure value outbox table: %w", err)
}
var keeperName *string
if row.KeeperName.Valid {
keeperName = &row.KeeperName.String
}
var externalID *string
if row.ExternalID.Valid {
externalID = &row.ExternalID.String
}
msg := contracts.OutboxMessage{
RequestID: row.RequestID,
Type: row.MessageType,
MessageID: row.MessageID,
Name: row.Name,
Namespace: row.Namespace,
KeeperName: keeperName,
ExternalID: externalID,
ReceiveCount: row.ReceiveCount,
Created: row.Created,
}
if row.MessageType != contracts.DeleteSecretOutboxMessage && row.EncryptedSecret.Valid {
msg.EncryptedSecret = row.EncryptedSecret.String
}
messages = append(messages, msg)
}
if err := rows.Err(); err != nil {
return messages, fmt.Errorf("reading rows: %w", err)
}
s.metrics.OutboxReceiveDuration.Observe(time.Since(start).Seconds())
s.metrics.OutboxReceiveCount.Add(float64(len(messages)))
return messages, nil
}
func (s *outboxStore) fetchMessageIdsInQueue(ctx context.Context, limit uint) ([]int64, error) {
req := fetchMessageIDsOutbox{
SQLTemplate: sqltemplate.New(s.dialect),
ReceiveLimit: limit,
}
query, err := sqltemplate.Execute(sqlSecureValueOutboxFetchMessageIDs, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxFetchMessageIDs.Name(), err)
}
rows, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil {
return nil, fmt.Errorf("fetching rows from secure value outbox table: %w", err)
}
defer func() { _ = rows.Close() }()
messageIDs := make([]int64, 0, limit)
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil {
return nil, fmt.Errorf("scanning row; %w", err)
}
messageIDs = append(messageIDs, id)
}
if err := rows.Err(); err != nil {
return nil, fmt.Errorf("reading rows: %w", err)
}
return messageIDs, nil
}
func (s *outboxStore) Delete(ctx context.Context, messageID int64) (err error) {
ctx, span := s.tracer.Start(ctx, "outboxStore.Append", trace.WithAttributes(
attribute.Int64("messageID", messageID),
))
defer span.End()
defer func() {
if err != nil {
span.SetStatus(codes.Error, "failed to delete message from outbox")
span.RecordError(err)
}
}()
assert.True(messageID != 0, "outboxStore.Delete: messageID is required")
start := time.Now()
if err := s.deleteMessage(ctx, messageID); err != nil {
return fmt.Errorf("deleting message from outbox table %+w", err)
}
s.metrics.OutboxDeleteDuration.Observe(time.Since(start).Seconds())
s.metrics.OutboxDeleteCount.Inc()
return nil
}
func (s *outboxStore) deleteMessage(ctx context.Context, messageID int64) error {
tsReq := getOutboxMessageTimestamp{
SQLTemplate: sqltemplate.New(s.dialect),
MessageID: messageID,
}
// First query the object so we can get the timestamp and calculate the total lifetime
timestampQuery, err := sqltemplate.Execute(sqlSecureValueOutboxQueryTimestamp, tsReq)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxQueryTimestamp.Name(), err)
}
rows, err := s.db.QueryContext(ctx, timestampQuery, tsReq.GetArgs()...)
if err != nil {
return fmt.Errorf("querying timestamp from secure value outbox table: %w", err)
}
if !rows.Next() {
_ = rows.Close()
return fmt.Errorf("no row found for message id=%v", messageID)
}
var timestamp int64
var messageType string
if err := rows.Scan(&timestamp, &messageType); err != nil {
_ = rows.Close()
return fmt.Errorf("scanning timestamp: %w", err)
}
// Explicitly close rows and check for errors before proceeding
if err := rows.Close(); err != nil {
return fmt.Errorf("closing rows: %w", err)
}
if err := rows.Err(); err != nil {
return fmt.Errorf("rows error: %w", err)
}
totalLifetime := time.Since(time.UnixMilli(timestamp))
s.metrics.OutboxTotalMessageLifetimeDuration.WithLabelValues(messageType).Observe(totalLifetime.Seconds())
// Then delete the object
delReq := deleteSecureValueOutbox{
SQLTemplate: sqltemplate.New(s.dialect),
MessageID: messageID,
}
query, err := sqltemplate.Execute(sqlSecureValueOutboxDelete, delReq)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxDelete.Name(), err)
}
result, err := s.db.ExecContext(ctx, query, delReq.GetArgs()...)
if err != nil {
return fmt.Errorf("deleting message id=%v from secure value outbox table: %w", messageID, err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("get rows affected: %w", err)
}
// TODO: Presumably it's a bug if we delete 0 rows?
if rowsAffected > 1 {
return fmt.Errorf("bug: deleted more than one row from the outbox table, should delete only one at a time: deleted=%v", rowsAffected)
}
return nil
}
func (s *outboxStore) IncrementReceiveCount(ctx context.Context, messageIDs []int64) error {
if len(messageIDs) == 0 {
return nil
}
req := incrementReceiveCountOutbox{
SQLTemplate: sqltemplate.New(s.dialect),
MessageIDs: messageIDs,
}
start := time.Now()
query, err := sqltemplate.Execute(sqlSecureValueOutboxUpdateReceiveCount, req)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueOutboxUpdateReceiveCount.Name(), err)
}
_, err = s.db.ExecContext(ctx, query, req.GetArgs()...)
if err != nil {
return fmt.Errorf("updating outbox messages receive count: %w", err)
}
s.metrics.OutboxIncrementReceiveCountDuration.Observe(time.Since(start).Seconds())
s.metrics.OutboxIncrementReceiveCountCount.Add(float64(len(messageIDs)))
return nil
}

@ -1,269 +0,0 @@
package metadata
import (
"context"
"fmt"
"math/rand"
"slices"
"testing"
"time"
"github.com/grafana/grafana/pkg/registry/apis/secret/contracts"
"github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/storage/secret/database"
"github.com/grafana/grafana/pkg/storage/secret/migrator"
"github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop"
)
type outboxStoreModel struct {
rows []contracts.OutboxMessage
}
func newOutboxStoreModel() *outboxStoreModel {
return &outboxStoreModel{}
}
func (model *outboxStoreModel) Append(messageID int64, message contracts.AppendOutboxMessage) {
model.rows = append(model.rows, contracts.OutboxMessage{
Type: message.Type,
MessageID: messageID,
Name: message.Name,
Namespace: message.Namespace,
EncryptedSecret: message.EncryptedSecret,
KeeperName: message.KeeperName,
ExternalID: message.ExternalID,
})
}
func (model *outboxStoreModel) ReceiveN(n uint) []contracts.OutboxMessage {
maxMessages := min(len(model.rows), int(n))
if maxMessages == 0 {
return nil
}
return model.rows[:maxMessages]
}
func (model *outboxStoreModel) Delete(messageID int64) {
oldLen := len(model.rows)
model.rows = slices.DeleteFunc(model.rows, func(m contracts.OutboxMessage) bool {
return m.MessageID == messageID
})
if len(model.rows) != oldLen-1 {
panic("Delete: deleted more than one message")
}
}
func TestOutboxStoreModel(t *testing.T) {
t.Parallel()
model := newOutboxStoreModel()
require.Empty(t, model.ReceiveN(10))
appendOutboxMessage := contracts.AppendOutboxMessage{
Type: contracts.CreateSecretOutboxMessage,
Name: "s-1",
Namespace: "n-1",
EncryptedSecret: "value",
ExternalID: nil,
}
outboxMessage1 := contracts.OutboxMessage{
MessageID: 1,
Type: contracts.CreateSecretOutboxMessage,
Name: "s-1",
Namespace: "n-1",
EncryptedSecret: "value",
ExternalID: nil,
}
outboxMessage2 := contracts.OutboxMessage{
MessageID: 2,
Type: contracts.CreateSecretOutboxMessage,
Name: "s-1",
Namespace: "n-1",
EncryptedSecret: "value",
ExternalID: nil,
}
model.Append(1, appendOutboxMessage)
require.Equal(t, []contracts.OutboxMessage{outboxMessage1}, model.ReceiveN(10))
model.Append(2, appendOutboxMessage)
require.Equal(t, []contracts.OutboxMessage{outboxMessage1, outboxMessage2}, model.ReceiveN(10))
model.Delete(outboxMessage1.MessageID)
require.Equal(t, []contracts.OutboxMessage{outboxMessage2}, model.ReceiveN(5))
model.Delete(outboxMessage2.MessageID)
require.Empty(t, model.ReceiveN(1))
}
func TestOutboxStoreSecureValueOperationInProgress(t *testing.T) {
t.Parallel()
t.Run("Append returns error when the queue already contains an operation for the secure value", func(t *testing.T) {
t.Parallel()
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
tracer := noop.NewTracerProvider().Tracer("test")
ctx := context.Background()
outbox := ProvideOutboxQueue(database.ProvideDatabase(testDB, tracer), tracer, nil)
_, err := outbox.Append(ctx, contracts.AppendOutboxMessage{
RequestID: "1",
Type: contracts.CreateSecretOutboxMessage,
Name: "name1",
Namespace: "ns1",
EncryptedSecret: "v1",
KeeperName: nil,
ExternalID: nil,
})
require.NoError(t, err)
_, err = outbox.Append(ctx, contracts.AppendOutboxMessage{
RequestID: "1",
Type: contracts.UpdateSecretOutboxMessage,
Name: "name1",
Namespace: "ns1",
EncryptedSecret: "v1",
KeeperName: nil,
ExternalID: nil,
})
require.ErrorIs(t, err, contracts.ErrSecureValueOperationInProgress)
})
}
func TestOutboxStore(t *testing.T) {
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
tracer := noop.NewTracerProvider().Tracer("test")
ctx := context.Background()
outbox := ProvideOutboxQueue(database.ProvideDatabase(testDB, tracer), tracer, nil)
m1 := contracts.AppendOutboxMessage{
Type: contracts.CreateSecretOutboxMessage,
Name: "s-1",
Namespace: "n-1",
EncryptedSecret: "value",
ExternalID: nil,
}
m2 := contracts.AppendOutboxMessage{
Type: contracts.CreateSecretOutboxMessage,
Name: "s-1",
Namespace: "n-2",
EncryptedSecret: "value",
ExternalID: nil,
}
messages, err := outbox.ReceiveN(ctx, 10)
require.NoError(t, err)
require.Empty(t, messages)
messageID1, err := outbox.Append(ctx, m1)
require.NoError(t, err)
for range 2 {
messages, err = outbox.ReceiveN(ctx, 10)
require.NoError(t, err)
require.Equal(t, 1, len(messages))
require.Equal(t, messageID1, messages[0].MessageID)
}
messageID2, err := outbox.Append(ctx, m2)
require.NoError(t, err)
messages, err = outbox.ReceiveN(ctx, 3)
require.NoError(t, err)
require.Equal(t, 2, len(messages))
require.Equal(t, messageID1, messages[0].MessageID)
require.Equal(t, messageID2, messages[1].MessageID)
require.NoError(t, outbox.Delete(ctx, messageID1))
messages, err = outbox.ReceiveN(ctx, 10)
require.NoError(t, err)
require.Equal(t, 1, len(messages))
require.Equal(t, messageID2, messages[0].MessageID)
require.NoError(t, outbox.Delete(ctx, messageID2))
messages, err = outbox.ReceiveN(ctx, 100)
require.NoError(t, err)
require.Empty(t, messages)
}
func TestOutboxStoreProperty(t *testing.T) {
seed := time.Now().UnixMicro()
rng := rand.New(rand.NewSource(seed))
defer func() {
if t.Failed() {
fmt.Printf("TestOutboxStoreProperty: SEED=%+v\n\n", seed)
}
}()
// The number of iterations was decided arbitrarily based on the time the test takes to run
for range 10 {
testDB := sqlstore.NewTestStore(t, sqlstore.WithMigrator(migrator.New()))
tracer := noop.NewTracerProvider().Tracer("test")
outbox := ProvideOutboxQueue(database.ProvideDatabase(testDB, tracer), tracer, nil)
model := newOutboxStoreModel()
ctx := context.Background()
for i := range 100 {
n := rng.Intn(3)
switch n {
case 0:
time.Sleep(1 * time.Microsecond)
message := contracts.AppendOutboxMessage{
Type: contracts.CreateSecretOutboxMessage,
Name: fmt.Sprintf("s-%d", i),
Namespace: fmt.Sprintf("n-%d", i),
EncryptedSecret: "value",
ExternalID: nil,
}
messageID, err := outbox.Append(ctx, message)
require.NoError(t, err)
model.Append(messageID, message)
case 1:
n := uint(rng.Intn(10))
messages, err := outbox.ReceiveN(ctx, n)
require.NoError(t, err)
modelMessages := model.ReceiveN(n)
require.Equal(t, len(modelMessages), len(messages))
for i := range len(modelMessages) {
require.Equal(t, modelMessages[i].MessageID, messages[i].MessageID)
}
case 2:
if len(model.rows) == 0 {
continue
}
message := model.rows[rng.Intn(len(model.rows))]
model.Delete(message.MessageID)
require.NoError(t, outbox.Delete(ctx, message.MessageID))
default:
panic(fmt.Sprintf("unhandled action: %+v", n))
}
}
}
}

@ -27,18 +27,12 @@ var (
sqlSecureValueRead = mustTemplate("secure_value_read.sql") sqlSecureValueRead = mustTemplate("secure_value_read.sql")
sqlSecureValueList = mustTemplate("secure_value_list.sql") sqlSecureValueList = mustTemplate("secure_value_list.sql")
sqlSecureValueCreate = mustTemplate("secure_value_create.sql") sqlSecureValueCreate = mustTemplate("secure_value_create.sql")
sqlSecureValueDelete = mustTemplate("secure_value_delete.sql")
sqlSecureValueUpdate = mustTemplate("secure_value_update.sql")
sqlSecureValueUpdateExternalId = mustTemplate("secure_value_updateExternalId.sql") sqlSecureValueUpdateExternalId = mustTemplate("secure_value_updateExternalId.sql")
sqlSecureValueUpdateStatus = mustTemplate("secure_value_updateStatus.sql")
sqlSecureValueReadForDecrypt = mustTemplate("secure_value_read_for_decrypt.sql") sqlSecureValueReadForDecrypt = mustTemplate("secure_value_read_for_decrypt.sql")
sqlSecureValueOutboxAppend = mustTemplate("secure_value_outbox_append.sql") sqlGetLatestSecureValueVersion = mustTemplate("secure_value_get_latest_version.sql")
sqlSecureValueOutboxFetchMessageIDs = mustTemplate("secure_value_outbox_fetch_message_ids.sql") sqlSecureValueSetVersionToActive = mustTemplate("secure_value_set_version_to_active.sql")
sqlSecureValueOutboxReceiveN = mustTemplate("secure_value_outbox_receiveN.sql") sqlSecureValueSetVersionToInactive = mustTemplate("secure_value_set_version_to_inactive.sql")
sqlSecureValueOutboxDelete = mustTemplate("secure_value_outbox_delete.sql")
sqlSecureValueOutboxUpdateReceiveCount = mustTemplate("secure_value_outbox_update_receive_count.sql")
sqlSecureValueOutboxQueryTimestamp = mustTemplate("secure_value_outbox_query_timestamp.sql")
) )
func mustTemplate(filename string) *template.Template { func mustTemplate(filename string) *template.Template {
@ -150,75 +144,69 @@ func (r readSecureValue) Validate() error {
return nil // TODO return nil // TODO
} }
type listSecureValue struct { type getLatestSecureValueVersion struct {
sqltemplate.SQLTemplate sqltemplate.SQLTemplate
Namespace string Namespace string
Name string
} }
// Validate is only used if we use `dbutil` from `unifiedstorage` func (r getLatestSecureValueVersion) Validate() error {
func (r listSecureValue) Validate() error { return nil
return nil // TODO
} }
type createSecureValue struct { type secureValueSetVersionToActive struct {
sqltemplate.SQLTemplate sqltemplate.SQLTemplate
Row *secureValueDB Namespace string
Name string
Version int64
} }
// Validate is only used if we use `dbutil` from `unifiedstorage` func (r secureValueSetVersionToActive) Validate() error {
func (r createSecureValue) Validate() error { return nil
return nil // TODO
} }
// Delete type secureValueSetVersionToInactive struct {
type deleteSecureValue struct {
sqltemplate.SQLTemplate sqltemplate.SQLTemplate
Namespace string Namespace string
Name string Name string
Version int64
} }
// Validate is only used if we use `dbutil` from `unifiedstorage` func (r secureValueSetVersionToInactive) Validate() error {
func (r deleteSecureValue) Validate() error { return nil
return nil // TODO
} }
// Update externalId type listSecureValue struct {
type updateExternalIdSecureValue struct {
sqltemplate.SQLTemplate sqltemplate.SQLTemplate
Namespace string Namespace string
Name string
ExternalID string
} }
// Validate is only used if we use `dbutil` from `unifiedstorage` // Validate is only used if we use `dbutil` from `unifiedstorage`
func (r updateExternalIdSecureValue) Validate() error { func (r listSecureValue) Validate() error {
return nil // TODO return nil // TODO
} }
// Update secure value type createSecureValue struct {
type updateSecureValue struct {
sqltemplate.SQLTemplate sqltemplate.SQLTemplate
Namespace string Row *secureValueDB
Name string
Row *secureValueDB
} }
// Validate is only used if we use `dbutil` from `unifiedstorage` // Validate is only used if we use `dbutil` from `unifiedstorage`
func (r updateSecureValue) Validate() error { func (r createSecureValue) Validate() error {
return nil // TODO return nil // TODO
} }
// update status message // Update externalId
type updateStatusSecureValue struct { type updateExternalIdSecureValue struct {
sqltemplate.SQLTemplate sqltemplate.SQLTemplate
Namespace string Namespace string
Name string Name string
Phase string Version int64
Message string ExternalID string
} }
// Validate is only used if we use `dbutil` from `unifiedstorage` // Validate is only used if we use `dbutil` from `unifiedstorage`
func (r updateStatusSecureValue) Validate() error { func (r updateExternalIdSecureValue) Validate() error {
return nil // TODO return nil // TODO
} }
@ -229,48 +217,3 @@ type readSecureValueForDecrypt struct {
} }
func (r readSecureValueForDecrypt) Validate() error { return nil } func (r readSecureValueForDecrypt) Validate() error { return nil }
/*************************************/
/**-- Secure Value Outbox Queries --**/
/*************************************/
type appendSecureValueOutbox struct {
sqltemplate.SQLTemplate
Row *outboxMessageDB
}
func (appendSecureValueOutbox) Validate() error { return nil }
type receiveNSecureValueOutbox struct {
sqltemplate.SQLTemplate
MessageIDs []int64
}
func (receiveNSecureValueOutbox) Validate() error { return nil }
type fetchMessageIDsOutbox struct {
sqltemplate.SQLTemplate
ReceiveLimit uint
}
func (fetchMessageIDsOutbox) Validate() error { return nil }
type deleteSecureValueOutbox struct {
sqltemplate.SQLTemplate
MessageID int64
}
func (deleteSecureValueOutbox) Validate() error { return nil }
type getOutboxMessageTimestamp struct {
sqltemplate.SQLTemplate
MessageID int64
}
func (getOutboxMessageTimestamp) Validate() error { return nil }
type incrementReceiveCountOutbox struct {
sqltemplate.SQLTemplate
MessageIDs []int64
}
func (incrementReceiveCountOutbox) Validate() error { return nil }

@ -1,7 +1,6 @@
package metadata package metadata
import ( import (
"database/sql"
"testing" "testing"
"text/template" "text/template"
@ -123,6 +122,27 @@ func TestSecureValueQueries(t *testing.T) {
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{ mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
RootDir: "testdata", RootDir: "testdata",
Templates: map[*template.Template][]mocks.TemplateTestCase{ Templates: map[*template.Template][]mocks.TemplateTestCase{
sqlGetLatestSecureValueVersion: {
{
Name: "get latest secure value version",
Data: &getLatestSecureValueVersion{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
},
},
},
sqlSecureValueSetVersionToActive: {
{
Name: "set secure value version to active",
Data: &secureValueSetVersionToActive{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
Version: 1,
},
},
},
sqlSecureValueRead: { sqlSecureValueRead: {
{ {
Name: "read", Name: "read",
@ -166,8 +186,7 @@ func TestSecureValueQueries(t *testing.T) {
CreatedBy: "user:ryan", CreatedBy: "user:ryan",
Updated: 5678, Updated: 5678,
UpdatedBy: "user:cameron", UpdatedBy: "user:cameron",
Phase: "creating", Version: 1,
Message: toNullString(nil),
Description: "description", Description: "description",
Keeper: toNullString(nil), Keeper: toNullString(nil),
Decrypters: toNullString(nil), Decrypters: toNullString(nil),
@ -190,72 +209,7 @@ func TestSecureValueQueries(t *testing.T) {
CreatedBy: "user:ryan", CreatedBy: "user:ryan",
Updated: 5678, Updated: 5678,
UpdatedBy: "user:cameron", UpdatedBy: "user:cameron",
Phase: "creating", Version: 1,
Message: toNullString(ptr.To("message_test")),
Description: "description",
Keeper: toNullString(ptr.To("keeper_test")),
Decrypters: toNullString(ptr.To("decrypters_test")),
Ref: toNullString(ptr.To("ref_test")),
ExternalID: "extId",
},
},
},
},
sqlSecureValueDelete: {
{
Name: "delete",
Data: &deleteSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
},
},
},
sqlSecureValueUpdate: {
{
Name: "update-null",
Data: &updateSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
Row: &secureValueDB{
GUID: "abc",
Name: "name",
Namespace: "ns",
Annotations: `{"x":"XXXX"}`,
Labels: `{"a":"AAA", "b", "BBBB"}`,
Created: 1234,
CreatedBy: "user:ryan",
Updated: 5678,
UpdatedBy: "user:cameron",
Phase: "creating",
Message: toNullString(nil),
Description: "description",
Keeper: toNullString(nil),
Decrypters: toNullString(nil),
Ref: toNullString(nil),
ExternalID: "extId",
},
},
},
{
Name: "update-not-null",
Data: &updateSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
Row: &secureValueDB{
GUID: "abc",
Name: "name",
Namespace: "ns",
Annotations: `{"x":"XXXX"}`,
Labels: `{"a":"AAA", "b", "BBBB"}`,
Created: 1234,
CreatedBy: "user:ryan",
Updated: 5678,
UpdatedBy: "user:cameron",
Phase: "creating",
Message: toNullString(ptr.To("message_test")),
Description: "description", Description: "description",
Keeper: toNullString(ptr.To("keeper_test")), Keeper: toNullString(ptr.To("keeper_test")),
Decrypters: toNullString(ptr.To("decrypters_test")), Decrypters: toNullString(ptr.To("decrypters_test")),
@ -276,18 +230,6 @@ func TestSecureValueQueries(t *testing.T) {
}, },
}, },
}, },
sqlSecureValueUpdateStatus: {
{
Name: "updateStatus",
Data: &updateStatusSecureValue{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Name: "name",
Namespace: "ns",
Phase: "Succeeded",
Message: "message-1",
},
},
},
sqlSecureValueReadForDecrypt: { sqlSecureValueReadForDecrypt: {
{ {
Name: "read-for-decrypt", Name: "read-for-decrypt",
@ -301,111 +243,3 @@ func TestSecureValueQueries(t *testing.T) {
}, },
}) })
} }
func TestSecureValueOutboxQueries(t *testing.T) {
mocks.CheckQuerySnapshots(t, mocks.TemplateTestSetup{
RootDir: "testdata",
Templates: map[*template.Template][]mocks.TemplateTestCase{
sqlSecureValueOutboxUpdateReceiveCount: {
{
Name: "update-receive-count",
Data: &incrementReceiveCountOutbox{
SQLTemplate: mocks.NewTestingSQLTemplate(),
MessageIDs: []int64{1, 2, 3},
},
},
},
sqlSecureValueOutboxAppend: {
{
Name: "no-encrypted-secret",
Data: &appendSecureValueOutbox{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Row: &outboxMessageDB{
MessageID: 1,
MessageType: "some-type",
Name: "name",
Namespace: "namespace",
ExternalID: sql.NullString{Valid: true, String: "external-id"},
KeeperName: sql.NullString{Valid: true, String: "keeper"},
Created: 1234,
},
},
},
{
Name: "no-external-id",
Data: &appendSecureValueOutbox{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Row: &outboxMessageDB{
MessageID: 1,
MessageType: "some-type",
Name: "name",
Namespace: "namespace",
EncryptedSecret: sql.NullString{Valid: true, String: "encrypted"},
KeeperName: sql.NullString{Valid: true, String: "keeper"},
Created: 1234,
},
},
},
{
Name: "no-keeper-name",
Data: &appendSecureValueOutbox{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Row: &outboxMessageDB{
MessageID: 1,
MessageType: "some-type",
Name: "name",
Namespace: "namespace",
EncryptedSecret: sql.NullString{Valid: true, String: "encrypted"},
ExternalID: sql.NullString{Valid: true, String: "external-id"},
Created: 1234,
},
},
},
{
Name: "all-fields-present",
Data: &appendSecureValueOutbox{
SQLTemplate: mocks.NewTestingSQLTemplate(),
Row: &outboxMessageDB{
MessageID: 1,
MessageType: "some-type",
Name: "name",
Namespace: "namespace",
EncryptedSecret: sql.NullString{Valid: true, String: "encrypted"},
ExternalID: sql.NullString{Valid: true, String: ""}, // can be empty string
KeeperName: sql.NullString{Valid: true, String: "keeper"},
Created: 1234,
},
},
},
},
sqlSecureValueOutboxFetchMessageIDs: {
{
Name: "basic",
Data: &fetchMessageIDsOutbox{
SQLTemplate: mocks.NewTestingSQLTemplate(),
ReceiveLimit: 10,
},
},
},
sqlSecureValueOutboxReceiveN: {
{
Name: "basic",
Data: &receiveNSecureValueOutbox{
SQLTemplate: mocks.NewTestingSQLTemplate(),
MessageIDs: []int64{1, 2, 3},
},
},
},
sqlSecureValueOutboxDelete: {
{
Name: "basic",
Data: &deleteSecureValueOutbox{
SQLTemplate: mocks.NewTestingSQLTemplate(),
MessageID: 1,
},
},
},
},
})
}

@ -29,8 +29,8 @@ type secureValueDB struct {
UpdatedBy string UpdatedBy string
// Kubernetes Status // Kubernetes Status
Phase string Active bool
Message sql.NullString Version int64
// Spec // Spec
Description string Description string
@ -74,8 +74,8 @@ func (sv *secureValueDB) toKubernetes() (*secretv0alpha1.SecureValue, error) {
Decrypters: decrypters, Decrypters: decrypters,
}, },
Status: secretv0alpha1.SecureValueStatus{ Status: secretv0alpha1.SecureValueStatus{
Phase: secretv0alpha1.SecureValuePhase(sv.Phase),
ExternalID: sv.ExternalID, ExternalID: sv.ExternalID,
Version: sv.Version,
}, },
} }
@ -85,10 +85,7 @@ func (sv *secureValueDB) toKubernetes() (*secretv0alpha1.SecureValue, error) {
if sv.Ref.Valid { if sv.Ref.Valid {
resource.Spec.Ref = &sv.Ref.String resource.Spec.Ref = &sv.Ref.String
} }
if sv.Message.Valid {
resource.Status.Message = sv.Message.String
}
resource.Status.Phase = secretv0alpha1.SecureValuePhase(sv.Phase)
resource.Status.ExternalID = sv.ExternalID resource.Status.ExternalID = sv.ExternalID
// Set all meta fields here for consistency. // Set all meta fields here for consistency.
@ -131,24 +128,6 @@ func toCreateRow(sv *secretv0alpha1.SecureValue, actorUID string) (*secureValueD
return row, nil return row, nil
} }
// toUpdateRow maps a Kubernetes resource into a DB row for existing resources being updated.
func toUpdateRow(currentRow *secureValueDB, newSecureValue *secretv0alpha1.SecureValue, actorUID, externalID string) (*secureValueDB, error) {
row, err := toRow(newSecureValue, externalID)
if err != nil {
return nil, fmt.Errorf("failed to create: %w", err)
}
now := time.Now().UTC().Unix()
row.GUID = currentRow.GUID
row.Created = currentRow.Created
row.CreatedBy = currentRow.CreatedBy
row.Updated = now
row.UpdatedBy = actorUID
return row, nil
}
// toRow maps a Kubernetes resource into a DB row. // toRow maps a Kubernetes resource into a DB row.
func toRow(sv *secretv0alpha1.SecureValue, externalID string) (*secureValueDB, error) { func toRow(sv *secretv0alpha1.SecureValue, externalID string) (*secureValueDB, error) {
var annotations string var annotations string
@ -201,11 +180,6 @@ func toRow(sv *secretv0alpha1.SecureValue, externalID string) (*secureValueDB, e
return nil, fmt.Errorf("failed to get resource version: %w", err) return nil, fmt.Errorf("failed to get resource version: %w", err)
} }
var statusMessage *string
if sv.Status.Message != "" {
statusMessage = &sv.Status.Message
}
return &secureValueDB{ return &secureValueDB{
GUID: string(sv.UID), GUID: string(sv.UID),
Name: sv.Name, Name: sv.Name,
@ -217,8 +191,7 @@ func toRow(sv *secretv0alpha1.SecureValue, externalID string) (*secureValueDB, e
Updated: updatedTimestamp, Updated: updatedTimestamp,
UpdatedBy: meta.GetUpdatedBy(), UpdatedBy: meta.GetUpdatedBy(),
Phase: string(sv.Status.Phase), Version: sv.Status.Version,
Message: toNullString(statusMessage),
Description: sv.Spec.Description, Description: sv.Spec.Description,
Keeper: toNullString(sv.Spec.Keeper), Keeper: toNullString(sv.Spec.Keeper),
@ -233,6 +206,7 @@ type secureValueForDecrypt struct {
Keeper sql.NullString Keeper sql.NullString
Decrypters sql.NullString Decrypters sql.NullString
Ref sql.NullString Ref sql.NullString
Active bool
ExternalID string ExternalID string
} }

@ -55,33 +55,18 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alp
)) ))
defer span.End() defer span.End()
sv.Status.Phase = secretv0alpha1.SecureValuePhasePending // Set inside of the transaction callback
sv.Status.Message = "Creating secure value" var row *secureValueDB
row, err := toCreateRow(sv, actorUID) err := s.db.Transaction(ctx, func(ctx context.Context) error {
if err != nil { if sv.Spec.Keeper != nil {
return nil, fmt.Errorf("to create row: %w", err)
}
req := createSecureValue{
SQLTemplate: sqltemplate.New(s.dialect),
Row: row,
}
query, err := sqltemplate.Execute(sqlSecureValueCreate, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueCreate.Name(), err)
}
err = s.db.Transaction(ctx, func(ctx context.Context) error {
if row.Keeper.Valid {
// Validate before inserting that the chosen `keeper` exists. // Validate before inserting that the chosen `keeper` exists.
// -- This is a copy of KeeperMetadataStore.read, which is not public at the moment, and is not defined in contract.KeeperMetadataStorage // -- This is a copy of KeeperMetadataStore.read, which is not public at the moment, and is not defined in contract.KeeperMetadataStorage
req := &readKeeper{ req := &readKeeper{
SQLTemplate: sqltemplate.New(s.dialect), SQLTemplate: sqltemplate.New(s.dialect),
Namespace: row.Namespace, Namespace: sv.Namespace,
Name: row.Keeper.String, Name: *sv.Spec.Keeper,
IsForUpdate: true, IsForUpdate: true,
} }
@ -101,23 +86,61 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alp
} }
} }
res, err := s.db.ExecContext(ctx, query, req.GetArgs()...) latestVersion, err := s.getLatestVersion(ctx, xkube.Namespace(sv.Namespace), sv.Name)
if err != nil { if err != nil {
if sql.IsRowAlreadyExistsError(err) { return fmt.Errorf("fetching latest secure value version: %w", err)
return fmt.Errorf("namespace=%+v name=%+v %w", sv.Namespace, sv.Name, contracts.ErrSecureValueAlreadyExists)
}
return fmt.Errorf("inserting row: %w", err)
} }
rowsAffected, err := res.RowsAffected() version := int64(1)
if err != nil { if latestVersion != nil {
return fmt.Errorf("getting rows affected: %w", err) version = *latestVersion + 1
} }
if rowsAffected != 1 { // Some other concurrent request may have created the version we're trying to create,
return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, row.Name, row.Namespace) // if that's the case, we'll retry with a new version up to max attempts.
maxAttempts := 3
attempts := 0
for {
sv.Status.Version = version
row, err = toCreateRow(sv, actorUID)
if err != nil {
return fmt.Errorf("to create row: %w", err)
}
req := createSecureValue{
SQLTemplate: sqltemplate.New(s.dialect),
Row: row,
}
query, err := sqltemplate.Execute(sqlSecureValueCreate, req)
if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueCreate.Name(), err)
}
res, err := s.db.ExecContext(ctx, query, req.GetArgs()...)
if err != nil {
if sql.IsRowAlreadyExistsError(err) {
if attempts < maxAttempts {
attempts += 1
version += 1
continue
}
return fmt.Errorf("namespace=%+v name=%+v %w", sv.Namespace, sv.Name, contracts.ErrSecureValueAlreadyExists)
}
return fmt.Errorf("inserting row: %w", err)
}
rowsAffected, err := res.RowsAffected()
if err != nil {
return fmt.Errorf("getting rows affected: %w", err)
}
if rowsAffected != 1 {
return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, row.Name, row.Namespace)
}
return nil
} }
return nil
}) })
if err != nil { if err != nil {
return nil, fmt.Errorf("db failure: %w", err) return nil, fmt.Errorf("db failure: %w", err)
@ -134,157 +157,161 @@ func (s *secureValueMetadataStorage) Create(ctx context.Context, sv *secretv0alp
return createdSecureValue, nil return createdSecureValue, nil
} }
func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv0alpha1.SecureValue, error) { func (s *secureValueMetadataStorage) getLatestVersion(ctx context.Context, namespace xkube.Namespace, name string) (*int64, error) {
start := time.Now() ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.getLatestVersion", trace.WithAttributes(
ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Read", trace.WithAttributes(
attribute.String("name", name), attribute.String("name", name),
attribute.String("namespace", namespace.String()), attribute.String("namespace", namespace.String()),
attribute.Bool("isForUpdate", opts.ForUpdate),
)) ))
defer span.End() defer span.End()
secureValue, err := s.read(ctx, namespace, name, opts) req := getLatestSecureValueVersion{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(),
Name: name,
}
q, err := sqltemplate.Execute(sqlGetLatestSecureValueVersion, req)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("execute template %q: %w", sqlGetLatestSecureValueVersion.Name(), err)
} }
secureValueKub, err := secureValue.toKubernetes() rows, err := s.db.QueryContext(ctx, q, req.GetArgs()...)
if err != nil { if err != nil {
return nil, fmt.Errorf("convert to kubernetes object: %w", err) return nil, fmt.Errorf("fetching latest version for secure value: namespace=%+v name=%+v %w", namespace, name, err)
} }
defer func() { _ = rows.Close() }()
s.metrics.SecureValueMetadataGetDuration.Observe(time.Since(start).Seconds()) if err := rows.Err(); err != nil {
s.metrics.SecureValueMetadataGetCount.Inc() return nil, fmt.Errorf("error executing query: %w", err)
}
return secureValueKub, nil if !rows.Next() {
return nil, nil
}
var version int64
if err := rows.Scan(&version); err != nil {
return nil, fmt.Errorf("scanning version from returned rows: %w", err)
}
return &version, nil
} }
func (s *secureValueMetadataStorage) Update(ctx context.Context, newSecureValue *secretv0alpha1.SecureValue, actorUID string) (*secretv0alpha1.SecureValue, error) { func (s *secureValueMetadataStorage) ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*contracts.DecryptSecureValue, error) {
start := time.Now() start := time.Now()
ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Update", trace.WithAttributes( ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.ReadForDecrypt", trace.WithAttributes(
attribute.String("name", newSecureValue.GetName()), attribute.String("name", name),
attribute.String("namespace", newSecureValue.GetNamespace()), attribute.String("namespace", namespace.String()),
attribute.String("actorUID", actorUID),
)) ))
defer span.End() defer span.End()
var newRow *secureValueDB req := readSecureValueForDecrypt{
SQLTemplate: sqltemplate.New(s.dialect),
err := s.db.Transaction(ctx, func(ctx context.Context) error { Namespace: namespace.String(),
read, err := s.read(ctx, xkube.Namespace(newSecureValue.Namespace), newSecureValue.Name, contracts.ReadOpts{ForUpdate: true}) Name: name,
if err != nil { }
return fmt.Errorf("reading secure value: %w", err)
}
// TODO: Confirm the ExternalID should come from the read model.
var updateErr error
newRow, updateErr = toUpdateRow(&read, newSecureValue, actorUID, read.ExternalID)
if updateErr != nil {
return fmt.Errorf("model to update row: %w", updateErr)
}
if newRow.Keeper.Valid {
// Validate before updating that the new `keeper` exists.
// -- This is a copy of KeeperMetadataStore.read, which is not public at the moment, and is not defined in contract.KeeperMetadataStorage query, err := sqltemplate.Execute(sqlSecureValueReadForDecrypt, req)
req := &readKeeper{ if err != nil {
SQLTemplate: sqltemplate.New(s.dialect), return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueReadForDecrypt.Name(), err)
Namespace: newRow.Namespace, }
Name: newRow.Keeper.String,
IsForUpdate: true,
}
query, err := sqltemplate.Execute(sqlKeeperRead, req) res, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil { if err != nil {
return fmt.Errorf("execute template %q: %w", sqlKeeperRead.Name(), err) return nil, fmt.Errorf("reading row: %w", err)
} }
defer func() { _ = res.Close() }()
res, err := s.db.QueryContext(ctx, query, req.GetArgs()...) var row secureValueForDecrypt
if err != nil { if !res.Next() {
return fmt.Errorf("getting row: %w", err) return nil, contracts.ErrSecureValueNotFound
} }
defer func() { _ = res.Close() }() if err := res.Scan(
&row.Keeper, &row.Decrypters,
&row.Ref, &row.ExternalID, &row.Active); err != nil {
return nil, fmt.Errorf("failed to scan secure value row: %w", err)
}
if !res.Next() { if err := res.Err(); err != nil {
return contracts.ErrKeeperNotFound return nil, fmt.Errorf("read rows error: %w", err)
} }
}
req := &updateSecureValue{ if !row.Active {
SQLTemplate: sqltemplate.New(s.dialect), return nil, fmt.Errorf("bug: read an inactive version: row=%+v", row)
Namespace: newRow.Namespace, }
Name: newRow.Name,
Row: newRow,
}
query, err := sqltemplate.Execute(sqlSecureValueUpdate, req) secureValue, err := row.toDecrypt()
if err != nil { if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdate.Name(), err) return nil, fmt.Errorf("convert to kubernetes object: %w", err)
} }
result, err := s.db.ExecContext(ctx, query, req.GetArgs()...) s.metrics.SecureValueGetForDecryptDuration.Observe(time.Since(start).Seconds())
if err != nil {
return fmt.Errorf("updating row: %w", err)
}
rowsAffected, err := result.RowsAffected() return secureValue, nil
if err != nil { }
return fmt.Errorf("getting rows affected: %w", err)
}
if rowsAffected != 1 { func (s *secureValueMetadataStorage) readActiveVersion(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (secureValueDB, error) {
return fmt.Errorf("expected 1 row affected, got %d for %s on %s", rowsAffected, newRow.Name, newRow.Namespace) req := readSecureValue{
} SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(),
Name: name,
IsForUpdate: opts.ForUpdate,
}
return nil query, err := sqltemplate.Execute(sqlSecureValueRead, req)
})
if err != nil { if err != nil {
return nil, fmt.Errorf("db failure: %w", err) return secureValueDB{}, fmt.Errorf("execute template %q: %w", sqlSecureValueRead.Name(), err)
} }
secureValue, err := newRow.toKubernetes() res, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil { if err != nil {
return nil, fmt.Errorf("convert to kubernetes object: %w", err) return secureValueDB{}, fmt.Errorf("reading row: %w", err)
} }
defer func() { _ = res.Close() }()
s.metrics.SecureValueMetadataUpdateDuration.Observe(time.Since(start).Seconds()) var secureValue secureValueDB
s.metrics.SecureValueMetadataUpdateCount.Inc() if !res.Next() {
return secureValueDB{}, contracts.ErrSecureValueNotFound
}
if err := res.Scan(
&secureValue.GUID, &secureValue.Name, &secureValue.Namespace,
&secureValue.Annotations, &secureValue.Labels,
&secureValue.Created, &secureValue.CreatedBy,
&secureValue.Updated, &secureValue.UpdatedBy,
&secureValue.Description, &secureValue.Keeper, &secureValue.Decrypters, &secureValue.Ref, &secureValue.ExternalID, &secureValue.Active, &secureValue.Version); err != nil {
return secureValueDB{}, fmt.Errorf("failed to scan secure value row: %w", err)
}
if err := res.Err(); err != nil {
return secureValueDB{}, fmt.Errorf("read rows error: %w", err)
}
return secureValue, nil return secureValue, nil
} }
func (s *secureValueMetadataStorage) Delete(ctx context.Context, namespace xkube.Namespace, name string) error { func (s *secureValueMetadataStorage) Read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (*secretv0alpha1.SecureValue, error) {
start := time.Now() start := time.Now()
ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Delete", trace.WithAttributes( ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.Read", trace.WithAttributes(
attribute.String("name", name), attribute.String("name", name),
attribute.String("namespace", namespace.String()), attribute.String("namespace", namespace.String()),
attribute.Bool("isForUpdate", opts.ForUpdate),
)) ))
defer span.End() defer span.End()
req := deleteSecureValue{ secureValue, err := s.readActiveVersion(ctx, namespace, name, opts)
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(),
Name: name,
}
query, err := sqltemplate.Execute(sqlSecureValueDelete, req)
if err != nil { if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueDelete.Name(), err) return nil, err
} }
res, err := s.db.ExecContext(ctx, query, req.GetArgs()...) secureValueKub, err := secureValue.toKubernetes()
if err != nil { if err != nil {
return fmt.Errorf("deleting secure value row: %w", err) return nil, fmt.Errorf("convert to kubernetes object: %w", err)
}
if rowsAffected, err := res.RowsAffected(); err != nil || rowsAffected != 1 {
return fmt.Errorf("deleting secure value rowsAffected=%d error=%w", rowsAffected, err)
} }
s.metrics.SecureValueMetadataDeleteDuration.Observe(time.Since(start).Seconds()) s.metrics.SecureValueMetadataGetDuration.Observe(time.Since(start).Seconds())
s.metrics.SecureValueMetadataDeleteCount.Inc() s.metrics.SecureValueMetadataGetCount.Inc()
return nil return secureValueKub, nil
} }
func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) (svList []secretv0alpha1.SecureValue, error error) { func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.Namespace) (svList []secretv0alpha1.SecureValue, error error) {
@ -323,15 +350,18 @@ func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.N
&row.Labels, &row.Labels,
&row.Created, &row.CreatedBy, &row.Created, &row.CreatedBy,
&row.Updated, &row.UpdatedBy, &row.Updated, &row.UpdatedBy,
&row.Phase, &row.Message,
&row.Description, &row.Keeper, &row.Decrypters, &row.Description, &row.Keeper, &row.Decrypters,
&row.Ref, &row.ExternalID, &row.Ref, &row.ExternalID, &row.Version, &row.Active,
) )
if err != nil { if err != nil {
return nil, fmt.Errorf("error reading secure value row: %w", err) return nil, fmt.Errorf("error reading secure value row: %w", err)
} }
if !row.Active {
return nil, fmt.Errorf("bug: read an inactive version: row=%+v", row)
}
secureValue, err := row.toKubernetes() secureValue, err := row.toKubernetes()
if err != nil { if err != nil {
return nil, fmt.Errorf("convert to kubernetes object: %w", err) return nil, fmt.Errorf("convert to kubernetes object: %w", err)
@ -349,172 +379,116 @@ func (s *secureValueMetadataStorage) List(ctx context.Context, namespace xkube.N
return secureValues, nil return secureValues, nil
} }
func (s *secureValueMetadataStorage) SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, externalID contracts.ExternalID) error { func (s *secureValueMetadataStorage) SetVersionToActive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error {
start := time.Now()
ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes( ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes(
attribute.String("name", name), attribute.String("name", name),
attribute.String("namespace", namespace.String()), attribute.String("namespace", namespace.String()),
attribute.String("externalID", externalID.String()), attribute.Int64("version", version),
)) ))
defer span.End() defer span.End()
req := updateExternalIdSecureValue{ req := secureValueSetVersionToActive{
SQLTemplate: sqltemplate.New(s.dialect), SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(), Namespace: namespace.String(),
Name: name, Name: name,
ExternalID: externalID.String(), Version: version,
} }
q, err := sqltemplate.Execute(sqlSecureValueUpdateExternalId, req) q, err := sqltemplate.Execute(sqlSecureValueSetVersionToActive, req)
if err != nil { if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdateExternalId.Name(), err) return fmt.Errorf("execute template %q: %w", sqlSecureValueSetVersionToActive.Name(), err)
} }
res, err := s.db.ExecContext(ctx, q, req.GetArgs()...) res, err := s.db.ExecContext(ctx, q, req.GetArgs()...)
if err != nil { if err != nil {
return fmt.Errorf("setting secure value external id: namespace=%+v name=%+v externalID=%+v %w", namespace, name, externalID, err) return fmt.Errorf("setting secure value version to active: namespace=%+v name=%+v version=%+v %w", namespace, name, version, err)
} }
// validate modified cound // validate modified cound
modifiedCount, err := res.RowsAffected() modifiedCount, err := res.RowsAffected()
if err != nil { if err != nil {
return fmt.Errorf("getting updated rows update external id secure value: %w", err) return fmt.Errorf("fetching number of modified rows: %w", err)
} }
if modifiedCount > 1 { if modifiedCount == 0 {
return fmt.Errorf("secureValueMetadataStorage.SetExternalID: modified more than one secret, this is a bug, check the where condition: modifiedCount=%d", modifiedCount) return fmt.Errorf("expected to modify at least one row but modified 0: modifiedCount=%d", modifiedCount)
} }
s.metrics.SecureValueSetExternalIDDuration.Observe(time.Since(start).Seconds())
return nil return nil
} }
func (s *secureValueMetadataStorage) SetStatus(ctx context.Context, namespace xkube.Namespace, name string, status secretv0alpha1.SecureValueStatus) error { func (s *secureValueMetadataStorage) SetVersionToInactive(ctx context.Context, namespace xkube.Namespace, name string, version int64) error {
start := time.Now() ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes(
ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetStatus", trace.WithAttributes(
attribute.String("name", name), attribute.String("name", name),
attribute.String("namespace", namespace.String()), attribute.String("namespace", namespace.String()),
attribute.String("status.phase", string(status.Phase)), attribute.Int64("version", version),
attribute.String("status.message", status.Message),
attribute.String("status.externalID", status.ExternalID),
)) ))
defer span.End() defer span.End()
req := updateStatusSecureValue{ req := secureValueSetVersionToInactive{
SQLTemplate: sqltemplate.New(s.dialect), SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(), Namespace: namespace.String(),
Name: name, Name: name,
Phase: string(status.Phase), Version: version,
Message: status.Message,
} }
q, err := sqltemplate.Execute(sqlSecureValueUpdateStatus, req) q, err := sqltemplate.Execute(sqlSecureValueSetVersionToInactive, req)
if err != nil { if err != nil {
return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdateStatus.Name(), err) return fmt.Errorf("execute template %q: %w", sqlSecureValueSetVersionToInactive.Name(), err)
} }
res, err := s.db.ExecContext(ctx, q, req.GetArgs()...) res, err := s.db.ExecContext(ctx, q, req.GetArgs()...)
if err != nil { if err != nil {
return fmt.Errorf("setting secure value status to Succeeded id: namespace=%+v name=%+v %w", namespace, name, err) return fmt.Errorf("setting secure value version to active: namespace=%+v name=%+v version=%+v %w", namespace, name, version, err)
} }
// validate modified cound
modifiedCount, err := res.RowsAffected() modifiedCount, err := res.RowsAffected()
if err != nil { if err != nil {
return fmt.Errorf("getting updated rows update status secure value: %w", err) return fmt.Errorf("fetching number of modified rows: %w", err)
} }
if modifiedCount > 1 { if modifiedCount > 1 {
return fmt.Errorf("secureValueMetadataStorage.SetExternalID: modified more than one secret, this is a bug, check the where condition: modifiedCount=%d", modifiedCount) return fmt.Errorf("expected to modify at at most one row but modified more: modifiedCount=%d", modifiedCount)
} }
s.metrics.SecureValueSetStatusDuration.Observe(time.Since(start).Seconds())
return nil return nil
} }
func (s *secureValueMetadataStorage) ReadForDecrypt(ctx context.Context, namespace xkube.Namespace, name string) (*contracts.DecryptSecureValue, error) { func (s *secureValueMetadataStorage) SetExternalID(ctx context.Context, namespace xkube.Namespace, name string, version int64, externalID contracts.ExternalID) error {
start := time.Now() start := time.Now()
ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.ReadForDecrypt", trace.WithAttributes( ctx, span := s.tracer.Start(ctx, "SecureValueMetadataStorage.SetExternalID", trace.WithAttributes(
attribute.String("name", name), attribute.String("name", name),
attribute.String("namespace", namespace.String()), attribute.String("namespace", namespace.String()),
attribute.String("externalID", externalID.String()),
attribute.Int64("version", version),
)) ))
defer span.End() defer span.End()
req := readSecureValueForDecrypt{ req := updateExternalIdSecureValue{
SQLTemplate: sqltemplate.New(s.dialect), SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(), Namespace: namespace.String(),
Name: name, Name: name,
Version: version,
ExternalID: externalID.String(),
} }
query, err := sqltemplate.Execute(sqlSecureValueReadForDecrypt, req) q, err := sqltemplate.Execute(sqlSecureValueUpdateExternalId, req)
if err != nil {
return nil, fmt.Errorf("execute template %q: %w", sqlSecureValueReadForDecrypt.Name(), err)
}
res, err := s.db.QueryContext(ctx, query, req.GetArgs()...)
if err != nil {
return nil, fmt.Errorf("reading row: %w", err)
}
defer func() { _ = res.Close() }()
var row secureValueForDecrypt
if !res.Next() {
return nil, contracts.ErrSecureValueNotFound
}
if err := res.Scan(
&row.Keeper, &row.Decrypters,
&row.Ref, &row.ExternalID); err != nil {
return nil, fmt.Errorf("failed to scan secure value row: %w", err)
}
if err := res.Err(); err != nil {
return nil, fmt.Errorf("read rows error: %w", err)
}
secureValue, err := row.toDecrypt()
if err != nil { if err != nil {
return nil, fmt.Errorf("convert to kubernetes object: %w", err) return fmt.Errorf("execute template %q: %w", sqlSecureValueUpdateExternalId.Name(), err)
}
s.metrics.SecureValueGetForDecryptDuration.Observe(time.Since(start).Seconds())
return secureValue, nil
}
func (s *secureValueMetadataStorage) read(ctx context.Context, namespace xkube.Namespace, name string, opts contracts.ReadOpts) (secureValueDB, error) {
req := readSecureValue{
SQLTemplate: sqltemplate.New(s.dialect),
Namespace: namespace.String(),
Name: name,
IsForUpdate: opts.ForUpdate,
} }
query, err := sqltemplate.Execute(sqlSecureValueRead, req) res, err := s.db.ExecContext(ctx, q, req.GetArgs()...)
if err != nil { if err != nil {
return secureValueDB{}, fmt.Errorf("execute template %q: %w", sqlSecureValueRead.Name(), err) return fmt.Errorf("setting secure value external id: namespace=%+v name=%+v externalID=%+v %w", namespace, name, externalID, err)
} }
res, err := s.db.QueryContext(ctx, query, req.GetArgs()...) // validate modified cound
modifiedCount, err := res.RowsAffected()
if err != nil { if err != nil {
return secureValueDB{}, fmt.Errorf("reading row: %w", err) return fmt.Errorf("getting updated rows update external id secure value: %w", err)
}
defer func() { _ = res.Close() }()
var secureValue secureValueDB
if !res.Next() {
return secureValueDB{}, contracts.ErrSecureValueNotFound
} }
if modifiedCount > 1 {
if err := res.Scan( return fmt.Errorf("secureValueMetadataStorage.SetExternalID: modified more than one secret, this is a bug, check the where condition: modifiedCount=%d", modifiedCount)
&secureValue.GUID, &secureValue.Name, &secureValue.Namespace,
&secureValue.Annotations, &secureValue.Labels,
&secureValue.Created, &secureValue.CreatedBy,
&secureValue.Updated, &secureValue.UpdatedBy,
&secureValue.Phase, &secureValue.Message,
&secureValue.Description, &secureValue.Keeper, &secureValue.Decrypters, &secureValue.Ref, &secureValue.ExternalID); err != nil {
return secureValueDB{}, fmt.Errorf("failed to scan secure value row: %w", err)
} }
s.metrics.SecureValueSetExternalIDDuration.Observe(time.Since(start).Seconds())
if err := res.Err(); err != nil { return nil
return secureValueDB{}, fmt.Errorf("read rows error: %w", err)
}
return secureValue, nil
} }

@ -1,4 +1,4 @@
package metadata package metadata_test
import ( import (
"context" "context"
@ -10,6 +10,7 @@ import (
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/sqlstore"
"github.com/grafana/grafana/pkg/storage/secret/database" "github.com/grafana/grafana/pkg/storage/secret/database"
"github.com/grafana/grafana/pkg/storage/secret/metadata"
"github.com/grafana/grafana/pkg/storage/secret/migrator" "github.com/grafana/grafana/pkg/storage/secret/migrator"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.opentelemetry.io/otel/trace/noop" "go.opentelemetry.io/otel/trace/noop"
@ -43,11 +44,11 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) {
features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform) features := featuremgmt.WithFeatures(featuremgmt.FlagGrafanaAPIServerWithExperimentalAPIs, featuremgmt.FlagSecretsManagementAppPlatform)
// Initialize the secure value storage // Initialize the secure value storage
secureValueStorage, err := ProvideSecureValueMetadataStorage(db, tracer, features, nil) secureValueStorage, err := metadata.ProvideSecureValueMetadataStorage(db, tracer, features, nil)
require.NoError(t, err) require.NoError(t, err)
// Initialize the keeper storage // Initialize the keeper storage
keeperStorage, err := ProvideKeeperMetadataStorage(db, tracer, features, nil) keeperStorage, err := metadata.ProvideKeeperMetadataStorage(db, tracer, features, nil)
require.NoError(t, err) require.NoError(t, err)
t.Run("create and read a secure value", func(t *testing.T) { t.Run("create and read a secure value", func(t *testing.T) {
@ -73,7 +74,8 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) {
require.Equal(t, "default", createdSecureValue.Namespace) require.Equal(t, "default", createdSecureValue.Namespace)
require.Equal(t, "test description", createdSecureValue.Spec.Description) require.Equal(t, "test description", createdSecureValue.Spec.Description)
require.Equal(t, keeperName, *createdSecureValue.Spec.Keeper) require.Equal(t, keeperName, *createdSecureValue.Spec.Keeper)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, createdSecureValue.Status.Phase)
require.NoError(t, secureValueStorage.SetVersionToActive(ctx, xkube.Namespace(createdSecureValue.Namespace), createdSecureValue.Name, createdSecureValue.Status.Version))
// Read the secure value back // Read the secure value back
readSecureValue, err := secureValueStorage.Read(ctx, xkube.Namespace("default"), "sv-test", contracts.ReadOpts{}) readSecureValue, err := secureValueStorage.Read(ctx, xkube.Namespace("default"), "sv-test", contracts.ReadOpts{})
@ -83,7 +85,6 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) {
require.Equal(t, "default", readSecureValue.Namespace) require.Equal(t, "default", readSecureValue.Namespace)
require.Equal(t, "test description", readSecureValue.Spec.Description) require.Equal(t, "test description", readSecureValue.Spec.Description)
require.Equal(t, keeperName, *readSecureValue.Spec.Keeper) require.Equal(t, keeperName, *readSecureValue.Spec.Keeper)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, readSecureValue.Status.Phase)
// List secure values and verify our value is in the list // List secure values and verify our value is in the list
secureValues, err := secureValueStorage.List(ctx, xkube.Namespace("default")) secureValues, err := secureValueStorage.List(ctx, xkube.Namespace("default"))
@ -98,7 +99,6 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) {
require.Equal(t, "default", sv.Namespace) require.Equal(t, "default", sv.Namespace)
require.Equal(t, "test description", sv.Spec.Description) require.Equal(t, "test description", sv.Spec.Description)
require.Equal(t, keeperName, *sv.Spec.Keeper) require.Equal(t, keeperName, *sv.Spec.Keeper)
require.Equal(t, secretv0alpha1.SecureValuePhasePending, sv.Status.Phase)
break break
} }
} }
@ -125,6 +125,8 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NotNil(t, createdSecureValue) require.NotNil(t, createdSecureValue)
require.NoError(t, secureValueStorage.SetVersionToActive(ctx, xkube.Namespace(createdSecureValue.Namespace), createdSecureValue.Name, createdSecureValue.Status.Version))
// Read the secure value to verify it exists // Read the secure value to verify it exists
readSecureValue, err := secureValueStorage.Read(ctx, xkube.Namespace("default"), "sv-test-2", contracts.ReadOpts{}) readSecureValue, err := secureValueStorage.Read(ctx, xkube.Namespace("default"), "sv-test-2", contracts.ReadOpts{})
require.NoError(t, err) require.NoError(t, err)
@ -132,7 +134,7 @@ func Test_SecureValueMetadataStorage_CreateAndRead(t *testing.T) {
require.Equal(t, "sv-test-2", readSecureValue.Name) require.Equal(t, "sv-test-2", readSecureValue.Name)
// Delete the secure value // Delete the secure value
err = secureValueStorage.Delete(ctx, xkube.Namespace("default"), "sv-test-2") err = secureValueStorage.SetVersionToInactive(ctx, xkube.Namespace("default"), "sv-test-2", readSecureValue.Status.Version)
require.NoError(t, err) require.NoError(t, err)
// Try to read the deleted secure value - should return error // Try to read the deleted secure value - should return error

@ -8,8 +8,8 @@ INSERT INTO `secret_secure_value` (
`created_by`, `created_by`,
`updated`, `updated`,
`updated_by`, `updated_by`,
`status_phase`, `active`,
`status_message`, `version`,
`description`, `description`,
`keeper`, `keeper`,
`decrypters`, `decrypters`,
@ -25,8 +25,8 @@ INSERT INTO `secret_secure_value` (
'user:ryan', 'user:ryan',
5678, 5678,
'user:cameron', 'user:cameron',
'creating', FALSE,
'message_test', 1,
'description', 'description',
'keeper_test', 'keeper_test',
'decrypters_test', 'decrypters_test',

@ -8,7 +8,8 @@ INSERT INTO `secret_secure_value` (
`created_by`, `created_by`,
`updated`, `updated`,
`updated_by`, `updated_by`,
`status_phase`, `active`,
`version`,
`description`, `description`,
`external_id` `external_id`
) VALUES ( ) VALUES (
@ -21,7 +22,8 @@ INSERT INTO `secret_secure_value` (
'user:ryan', 'user:ryan',
5678, 5678,
'user:cameron', 'user:cameron',
'creating', FALSE,
1,
'description', 'description',
'extId' 'extId'
); );

@ -1,4 +0,0 @@
DELETE FROM `secret_secure_value`
WHERE `namespace` = 'ns' AND
`name` = 'name'
;

@ -0,0 +1,10 @@
SELECT
`version`
FROM
`secret_secure_value`
WHERE
`namespace` = 'ns' AND
`name` = 'name'
ORDER BY `version` DESC
LIMIT 1
;

@ -8,15 +8,17 @@ SELECT
`created_by`, `created_by`,
`updated`, `updated`,
`updated_by`, `updated_by`,
`status_phase`,
`status_message`,
`description`, `description`,
`keeper`, `keeper`,
`decrypters`, `decrypters`,
`ref`, `ref`,
`external_id` `external_id`,
`version`,
`active`
FROM FROM
`secret_secure_value` `secret_secure_value`
WHERE `namespace` = 'ns' WHERE
`namespace` = 'ns' AND
`active` = true
ORDER BY `updated` DESC ORDER BY `updated` DESC
; ;

@ -3,7 +3,9 @@ SELECT
`keeper` `keeper`
FROM FROM
`secret_secure_value` `secret_secure_value`
WHERE `namespace` = 'ns' AND WHERE
`name` IN ('a', 'b') `namespace` = 'ns' AND
`name` IN ('a', 'b') AND
`active` = true
FOR UPDATE FOR UPDATE
; ;

@ -1,21 +0,0 @@
INSERT INTO `secret_secure_value_outbox` (
`request_id`,
`message_type`,
`name`,
`namespace`,
`encrypted_secret`,
`keeper_name`,
`external_id`,
`receive_count`,
`created`
) VALUES (
'',
'some-type',
'name',
'namespace',
'encrypted',
'keeper',
'',
0,
1234
);

@ -1,19 +0,0 @@
INSERT INTO `secret_secure_value_outbox` (
`request_id`,
`message_type`,
`name`,
`namespace`,
`keeper_name`,
`external_id`,
`receive_count`,
`created`
) VALUES (
'',
'some-type',
'name',
'namespace',
'keeper',
'external-id',
0,
1234
);

@ -1,19 +0,0 @@
INSERT INTO `secret_secure_value_outbox` (
`request_id`,
`message_type`,
`name`,
`namespace`,
`encrypted_secret`,
`keeper_name`,
`receive_count`,
`created`
) VALUES (
'',
'some-type',
'name',
'namespace',
'encrypted',
'keeper',
0,
1234
);

@ -1,19 +0,0 @@
INSERT INTO `secret_secure_value_outbox` (
`request_id`,
`message_type`,
`name`,
`namespace`,
`encrypted_secret`,
`external_id`,
`receive_count`,
`created`
) VALUES (
'',
'some-type',
'name',
'namespace',
'encrypted',
'external-id',
0,
1234
);

@ -1,5 +0,0 @@
DELETE FROM
`secret_secure_value_outbox`
WHERE
`id` = 1
;

@ -1,6 +0,0 @@
SELECT
`id`
FROM `secret_secure_value_outbox`
ORDER BY id ASC
LIMIT 10
;

@ -1,19 +0,0 @@
SELECT
`request_id`,
`id`,
`message_type`,
`name`,
`namespace`,
`encrypted_secret`,
`keeper_name`,
`external_id`,
`receive_count`,
`created`
FROM
`secret_secure_value_outbox`
WHERE
`id` IN (1, 2, 3)
ORDER BY
`id` ASC
FOR UPDATE SKIP LOCKED
;

@ -1,7 +0,0 @@
UPDATE
`secret_secure_value_outbox`
SET
`receive_count` = `receive_count` + 1
WHERE
`id` IN (1, 2, 3)
;

@ -8,16 +8,18 @@ SELECT
`created_by`, `created_by`,
`updated`, `updated`,
`updated_by`, `updated_by`,
`status_phase`,
`status_message`,
`description`, `description`,
`keeper`, `keeper`,
`decrypters`, `decrypters`,
`ref`, `ref`,
`external_id` `external_id`,
`active`,
`version`
FROM FROM
`secret_secure_value` `secret_secure_value`
WHERE `namespace` = 'ns' AND WHERE
`name` = 'name' `namespace` = 'ns' AND
`name` = 'name' AND
`active` = true
FOR UPDATE FOR UPDATE
; ;

@ -8,15 +8,17 @@ SELECT
`created_by`, `created_by`,
`updated`, `updated`,
`updated_by`, `updated_by`,
`status_phase`,
`status_message`,
`description`, `description`,
`keeper`, `keeper`,
`decrypters`, `decrypters`,
`ref`, `ref`,
`external_id` `external_id`,
`active`,
`version`
FROM FROM
`secret_secure_value` `secret_secure_value`
WHERE `namespace` = 'ns' AND WHERE
`name` = 'name' `namespace` = 'ns' AND
`name` = 'name' AND
`active` = true
; ;

@ -2,9 +2,12 @@ SELECT
`keeper`, `keeper`,
`decrypters`, `decrypters`,
`ref`, `ref`,
`external_id` `external_id`,
`active`
FROM FROM
`secret_secure_value` `secret_secure_value`
WHERE `namespace` = 'ns' AND WHERE
`name` = 'name' `namespace` = 'ns' AND
`name` = 'name' AND
`active` = true
; ;

@ -0,0 +1,8 @@
UPDATE
`secret_secure_value`
SET
`active` = (`version` = 1)
WHERE
`namespace` = 'ns' AND
`name` = 'name'
;

@ -1,22 +0,0 @@
UPDATE
`secret_secure_value`
SET
`guid` = 'abc',
`name` = 'name',
`namespace` = 'ns',
`annotations` = '{"x":"XXXX"}',
`labels` = '{"a":"AAA", "b", "BBBB"}',
`created` = 1234,
`created_by` = 'user:ryan',
`updated` = 5678,
`updated_by` = 'user:cameron',
`status_phase` = 'creating',
`status_message` = 'message_test',
`description` = 'description',
`keeper` = 'keeper_test',
`decrypters` = 'decrypters_test',
`ref` = 'ref_test',
`external_id` = 'extId'
WHERE `namespace` = 'ns' AND
`name` = 'name'
;

@ -1,18 +0,0 @@
UPDATE
`secret_secure_value`
SET
`guid` = 'abc',
`name` = 'name',
`namespace` = 'ns',
`annotations` = '{"x":"XXXX"}',
`labels` = '{"a":"AAA", "b", "BBBB"}',
`created` = 1234,
`created_by` = 'user:ryan',
`updated` = 5678,
`updated_by` = 'user:cameron',
`status_phase` = 'creating',
`description` = 'description',
`external_id` = 'extId'
WHERE `namespace` = 'ns' AND
`name` = 'name'
;

@ -2,6 +2,8 @@ UPDATE
`secret_secure_value` `secret_secure_value`
SET SET
`external_id` = 'extId' `external_id` = 'extId'
WHERE `namespace` = 'ns' AND WHERE
`name` = 'name' `namespace` = 'ns' AND
`name` = 'name' AND
`version` = 0
; ;

@ -1,8 +0,0 @@
UPDATE
`secret_secure_value`
SET
`status_phase` = 'Succeeded',
`status_message` = 'message-1'
WHERE `namespace` = 'ns' AND
`name` = 'name'
;

@ -8,8 +8,8 @@ INSERT INTO "secret_secure_value" (
"created_by", "created_by",
"updated", "updated",
"updated_by", "updated_by",
"status_phase", "active",
"status_message", "version",
"description", "description",
"keeper", "keeper",
"decrypters", "decrypters",
@ -25,8 +25,8 @@ INSERT INTO "secret_secure_value" (
'user:ryan', 'user:ryan',
5678, 5678,
'user:cameron', 'user:cameron',
'creating', FALSE,
'message_test', 1,
'description', 'description',
'keeper_test', 'keeper_test',
'decrypters_test', 'decrypters_test',

@ -8,7 +8,8 @@ INSERT INTO "secret_secure_value" (
"created_by", "created_by",
"updated", "updated",
"updated_by", "updated_by",
"status_phase", "active",
"version",
"description", "description",
"external_id" "external_id"
) VALUES ( ) VALUES (
@ -21,7 +22,8 @@ INSERT INTO "secret_secure_value" (
'user:ryan', 'user:ryan',
5678, 5678,
'user:cameron', 'user:cameron',
'creating', FALSE,
1,
'description', 'description',
'extId' 'extId'
); );

@ -1,4 +0,0 @@
DELETE FROM "secret_secure_value"
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

@ -0,0 +1,10 @@
SELECT
"version"
FROM
"secret_secure_value"
WHERE
"namespace" = 'ns' AND
"name" = 'name'
ORDER BY "version" DESC
LIMIT 1
;

@ -8,15 +8,17 @@ SELECT
"created_by", "created_by",
"updated", "updated",
"updated_by", "updated_by",
"status_phase",
"status_message",
"description", "description",
"keeper", "keeper",
"decrypters", "decrypters",
"ref", "ref",
"external_id" "external_id",
"version",
"active"
FROM FROM
"secret_secure_value" "secret_secure_value"
WHERE "namespace" = 'ns' WHERE
"namespace" = 'ns' AND
"active" = true
ORDER BY "updated" DESC ORDER BY "updated" DESC
; ;

@ -3,7 +3,9 @@ SELECT
"keeper" "keeper"
FROM FROM
"secret_secure_value" "secret_secure_value"
WHERE "namespace" = 'ns' AND WHERE
"name" IN ('a', 'b') "namespace" = 'ns' AND
"name" IN ('a', 'b') AND
"active" = true
FOR UPDATE FOR UPDATE
; ;

@ -1,21 +0,0 @@
INSERT INTO "secret_secure_value_outbox" (
"request_id",
"message_type",
"name",
"namespace",
"encrypted_secret",
"keeper_name",
"external_id",
"receive_count",
"created"
) VALUES (
'',
'some-type',
'name',
'namespace',
'encrypted',
'keeper',
'',
0,
1234
);

@ -1,19 +0,0 @@
INSERT INTO "secret_secure_value_outbox" (
"request_id",
"message_type",
"name",
"namespace",
"keeper_name",
"external_id",
"receive_count",
"created"
) VALUES (
'',
'some-type',
'name',
'namespace',
'keeper',
'external-id',
0,
1234
);

@ -1,19 +0,0 @@
INSERT INTO "secret_secure_value_outbox" (
"request_id",
"message_type",
"name",
"namespace",
"encrypted_secret",
"keeper_name",
"receive_count",
"created"
) VALUES (
'',
'some-type',
'name',
'namespace',
'encrypted',
'keeper',
0,
1234
);

@ -1,19 +0,0 @@
INSERT INTO "secret_secure_value_outbox" (
"request_id",
"message_type",
"name",
"namespace",
"encrypted_secret",
"external_id",
"receive_count",
"created"
) VALUES (
'',
'some-type',
'name',
'namespace',
'encrypted',
'external-id',
0,
1234
);

@ -1,5 +0,0 @@
DELETE FROM
"secret_secure_value_outbox"
WHERE
"id" = 1
;

@ -1,6 +0,0 @@
SELECT
"id"
FROM "secret_secure_value_outbox"
ORDER BY id ASC
LIMIT 10
;

@ -1,19 +0,0 @@
SELECT
"request_id",
"id",
"message_type",
"name",
"namespace",
"encrypted_secret",
"keeper_name",
"external_id",
"receive_count",
"created"
FROM
"secret_secure_value_outbox"
WHERE
"id" IN (1, 2, 3)
ORDER BY
"id" ASC
FOR UPDATE SKIP LOCKED
;

@ -1,7 +0,0 @@
UPDATE
"secret_secure_value_outbox"
SET
"receive_count" = "receive_count" + 1
WHERE
"id" IN (1, 2, 3)
;

@ -8,16 +8,18 @@ SELECT
"created_by", "created_by",
"updated", "updated",
"updated_by", "updated_by",
"status_phase",
"status_message",
"description", "description",
"keeper", "keeper",
"decrypters", "decrypters",
"ref", "ref",
"external_id" "external_id",
"active",
"version"
FROM FROM
"secret_secure_value" "secret_secure_value"
WHERE "namespace" = 'ns' AND WHERE
"name" = 'name' "namespace" = 'ns' AND
"name" = 'name' AND
"active" = true
FOR UPDATE FOR UPDATE
; ;

@ -8,15 +8,17 @@ SELECT
"created_by", "created_by",
"updated", "updated",
"updated_by", "updated_by",
"status_phase",
"status_message",
"description", "description",
"keeper", "keeper",
"decrypters", "decrypters",
"ref", "ref",
"external_id" "external_id",
"active",
"version"
FROM FROM
"secret_secure_value" "secret_secure_value"
WHERE "namespace" = 'ns' AND WHERE
"name" = 'name' "namespace" = 'ns' AND
"name" = 'name' AND
"active" = true
; ;

@ -2,9 +2,12 @@ SELECT
"keeper", "keeper",
"decrypters", "decrypters",
"ref", "ref",
"external_id" "external_id",
"active"
FROM FROM
"secret_secure_value" "secret_secure_value"
WHERE "namespace" = 'ns' AND WHERE
"name" = 'name' "namespace" = 'ns' AND
"name" = 'name' AND
"active" = true
; ;

@ -0,0 +1,8 @@
UPDATE
"secret_secure_value"
SET
"active" = ("version" = 1)
WHERE
"namespace" = 'ns' AND
"name" = 'name'
;

@ -1,22 +0,0 @@
UPDATE
"secret_secure_value"
SET
"guid" = 'abc',
"name" = 'name',
"namespace" = 'ns',
"annotations" = '{"x":"XXXX"}',
"labels" = '{"a":"AAA", "b", "BBBB"}',
"created" = 1234,
"created_by" = 'user:ryan',
"updated" = 5678,
"updated_by" = 'user:cameron',
"status_phase" = 'creating',
"status_message" = 'message_test',
"description" = 'description',
"keeper" = 'keeper_test',
"decrypters" = 'decrypters_test',
"ref" = 'ref_test',
"external_id" = 'extId'
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

@ -1,18 +0,0 @@
UPDATE
"secret_secure_value"
SET
"guid" = 'abc',
"name" = 'name',
"namespace" = 'ns',
"annotations" = '{"x":"XXXX"}',
"labels" = '{"a":"AAA", "b", "BBBB"}',
"created" = 1234,
"created_by" = 'user:ryan',
"updated" = 5678,
"updated_by" = 'user:cameron',
"status_phase" = 'creating',
"description" = 'description',
"external_id" = 'extId'
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

@ -2,6 +2,8 @@ UPDATE
"secret_secure_value" "secret_secure_value"
SET SET
"external_id" = 'extId' "external_id" = 'extId'
WHERE "namespace" = 'ns' AND WHERE
"name" = 'name' "namespace" = 'ns' AND
"name" = 'name' AND
"version" = 0
; ;

@ -1,8 +0,0 @@
UPDATE
"secret_secure_value"
SET
"status_phase" = 'Succeeded',
"status_message" = 'message-1'
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

@ -8,8 +8,8 @@ INSERT INTO "secret_secure_value" (
"created_by", "created_by",
"updated", "updated",
"updated_by", "updated_by",
"status_phase", "active",
"status_message", "version",
"description", "description",
"keeper", "keeper",
"decrypters", "decrypters",
@ -25,8 +25,8 @@ INSERT INTO "secret_secure_value" (
'user:ryan', 'user:ryan',
5678, 5678,
'user:cameron', 'user:cameron',
'creating', FALSE,
'message_test', 1,
'description', 'description',
'keeper_test', 'keeper_test',
'decrypters_test', 'decrypters_test',

@ -8,7 +8,8 @@ INSERT INTO "secret_secure_value" (
"created_by", "created_by",
"updated", "updated",
"updated_by", "updated_by",
"status_phase", "active",
"version",
"description", "description",
"external_id" "external_id"
) VALUES ( ) VALUES (
@ -21,7 +22,8 @@ INSERT INTO "secret_secure_value" (
'user:ryan', 'user:ryan',
5678, 5678,
'user:cameron', 'user:cameron',
'creating', FALSE,
1,
'description', 'description',
'extId' 'extId'
); );

@ -1,4 +0,0 @@
DELETE FROM "secret_secure_value"
WHERE "namespace" = 'ns' AND
"name" = 'name'
;

@ -0,0 +1,10 @@
SELECT
"version"
FROM
"secret_secure_value"
WHERE
"namespace" = 'ns' AND
"name" = 'name'
ORDER BY "version" DESC
LIMIT 1
;

@ -8,15 +8,17 @@ SELECT
"created_by", "created_by",
"updated", "updated",
"updated_by", "updated_by",
"status_phase",
"status_message",
"description", "description",
"keeper", "keeper",
"decrypters", "decrypters",
"ref", "ref",
"external_id" "external_id",
"version",
"active"
FROM FROM
"secret_secure_value" "secret_secure_value"
WHERE "namespace" = 'ns' WHERE
"namespace" = 'ns' AND
"active" = true
ORDER BY "updated" DESC ORDER BY "updated" DESC
; ;

@ -3,6 +3,8 @@ SELECT
"keeper" "keeper"
FROM FROM
"secret_secure_value" "secret_secure_value"
WHERE "namespace" = 'ns' AND WHERE
"name" IN ('a', 'b') "namespace" = 'ns' AND
"name" IN ('a', 'b') AND
"active" = true
; ;

@ -1,21 +0,0 @@
INSERT INTO "secret_secure_value_outbox" (
"request_id",
"message_type",
"name",
"namespace",
"encrypted_secret",
"keeper_name",
"external_id",
"receive_count",
"created"
) VALUES (
'',
'some-type',
'name',
'namespace',
'encrypted',
'keeper',
'',
0,
1234
);

@ -1,19 +0,0 @@
INSERT INTO "secret_secure_value_outbox" (
"request_id",
"message_type",
"name",
"namespace",
"keeper_name",
"external_id",
"receive_count",
"created"
) VALUES (
'',
'some-type',
'name',
'namespace',
'keeper',
'external-id',
0,
1234
);

@ -1,19 +0,0 @@
INSERT INTO "secret_secure_value_outbox" (
"request_id",
"message_type",
"name",
"namespace",
"encrypted_secret",
"keeper_name",
"receive_count",
"created"
) VALUES (
'',
'some-type',
'name',
'namespace',
'encrypted',
'keeper',
0,
1234
);

@ -1,19 +0,0 @@
INSERT INTO "secret_secure_value_outbox" (
"request_id",
"message_type",
"name",
"namespace",
"encrypted_secret",
"external_id",
"receive_count",
"created"
) VALUES (
'',
'some-type',
'name',
'namespace',
'encrypted',
'external-id',
0,
1234
);

@ -1,5 +0,0 @@
DELETE FROM
"secret_secure_value_outbox"
WHERE
"id" = 1
;

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save