diff --git a/CHANGELOG.md b/CHANGELOG.md index 3aa85a9b..875d234d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.10.1] - 2024-12-12 + +### Fixed + +- Fix grafana organization reordering. + ## [0.10.0] - 2024-12-10 ### Added @@ -188,7 +194,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Initialize project and create heartbeat for the installation. -[Unreleased]: https://github.com/giantswarm/observability-operator/compare/v0.10.0...HEAD +[Unreleased]: https://github.com/giantswarm/observability-operator/compare/v0.10.1...HEAD +[0.10.1]: https://github.com/giantswarm/observability-operator/compare/v0.10.0...v0.10.1 [0.10.0]: https://github.com/giantswarm/observability-operator/compare/v0.9.1...v0.10.0 [0.9.1]: https://github.com/giantswarm/observability-operator/compare/v0.9.0...v0.9.1 [0.9.0]: https://github.com/giantswarm/observability-operator/compare/v0.8.1...v0.9.0 diff --git a/api/v1alpha1/grafanaorganization_types.go b/api/v1alpha1/grafanaorganization_types.go index aebfe810..293c1340 100644 --- a/api/v1alpha1/grafanaorganization_types.go +++ b/api/v1alpha1/grafanaorganization_types.go @@ -29,6 +29,8 @@ const ( // GrafanaOrganizationSpec defines the desired state of GrafanaOrganization type GrafanaOrganizationSpec struct { // DisplayName is the name displayed when viewing the organization in Grafana. It can be different from the actual org's name. + // +kubebuilder:example="Giant Swarm" + // +kubebuilder:validation:MinLength=1 DisplayName string `json:"displayName"` // Access rules defines user permissions for interacting with the organization in Grafana. @@ -36,7 +38,8 @@ type GrafanaOrganizationSpec struct { // Tenants is a list of tenants that are associated with the Grafana organization. // +kubebuilder:example={"giantswarm"} - Tenants []TenantID `json:"tenants,omitempty"` + // +kube:validation:MinItems=1 + Tenants []TenantID `json:"tenants"` } // TenantID is a unique identifier for a tenant. It must be lowercase. diff --git a/config/crd/observability.giantswarm.io_grafanaorganizations.yaml b/config/crd/observability.giantswarm.io_grafanaorganizations.yaml index 31927d97..d98023d2 100644 --- a/config/crd/observability.giantswarm.io_grafanaorganizations.yaml +++ b/config/crd/observability.giantswarm.io_grafanaorganizations.yaml @@ -50,6 +50,8 @@ spec: displayName: description: DisplayName is the name displayed when viewing the organization in Grafana. It can be different from the actual org's name. + example: Giant Swarm + minLength: 1 type: string rbac: description: Access rules defines user permissions for interacting @@ -92,6 +94,7 @@ spec: required: - displayName - rbac + - tenants type: object status: description: GrafanaOrganizationStatus defines the observed state of GrafanaOrganization diff --git a/go.mod b/go.mod index a13d82fc..23060854 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/giantswarm/observability-operator -go 1.23 +go 1.23.0 toolchain go1.23.4 @@ -19,10 +19,10 @@ require ( github.com/prometheus/client_golang v1.20.5 github.com/prometheus/common v0.61.0 github.com/sirupsen/logrus v1.9.3 - k8s.io/api v0.31.3 - k8s.io/apimachinery v0.31.3 - k8s.io/client-go v0.31.3 - sigs.k8s.io/cluster-api v1.8.5 + k8s.io/api v0.32.0 + k8s.io/apimachinery v0.32.0 + k8s.io/client-go v0.32.0 + sigs.k8s.io/cluster-api v1.9.0 sigs.k8s.io/controller-runtime v0.19.3 sigs.k8s.io/yaml v1.4.0 ) @@ -40,9 +40,8 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/swag v0.23.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect - github.com/gobuffalo/flect v1.0.2 // indirect + github.com/gobuffalo/flect v1.0.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.4 // indirect github.com/google/gnostic-models v0.6.8 // indirect github.com/google/go-cmp v0.6.0 // indirect @@ -51,7 +50,6 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect - github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect @@ -63,25 +61,25 @@ require ( github.com/spf13/pflag v1.0.5 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/crypto v0.30.0 + golang.org/x/crypto v0.31.0 golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e // indirect golang.org/x/net v0.32.0 // indirect golang.org/x/oauth2 v0.24.0 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/term v0.27.0 // indirect golang.org/x/text v0.21.0 // indirect - golang.org/x/time v0.6.0 // indirect + golang.org/x/time v0.7.0 // indirect golang.org/x/tools v0.26.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect google.golang.org/protobuf v1.35.2 // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 - k8s.io/apiextensions-apiserver v0.31.2 // indirect + k8s.io/apiextensions-apiserver v0.31.3 // indirect k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 // indirect + k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f // indirect k8s.io/utils v0.0.0-20241210054802-24370beab758 - sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect + sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/structured-merge-diff/v4 v4.4.2 // indirect ) require ( @@ -113,7 +111,7 @@ require ( go.opentelemetry.io/otel/metric v1.28.0 // indirect go.opentelemetry.io/otel/trace v1.28.0 // indirect golang.org/x/sync v0.10.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect + gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect ) replace ( diff --git a/go.sum b/go.sum index 44cdcbf3..858d460e 100644 --- a/go.sum +++ b/go.sum @@ -26,8 +26,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coredns/caddy v1.1.1 h1:2eYKZT7i6yxIfGP3qLJoJ7HAsDJqYB+X68g4NYjSrE0= github.com/coredns/caddy v1.1.1/go.mod h1:A6ntJQlAWuQfFlsd9hvigKbo2WS0VUs2l1e2F+BawD4= -github.com/coredns/corefile-migration v1.0.23 h1:Fp4FETmk8sT/IRgnKX2xstC2dL7+QdcU+BL5AYIN3Jw= -github.com/coredns/corefile-migration v1.0.23/go.mod h1:8HyMhuyzx9RLZp8cRc9Uf3ECpEAafHOFxQWUPqktMQI= +github.com/coredns/corefile-migration v1.0.24 h1:NL/zRKijhJZLYlNnMr891DRv5jXgfd3Noons1M6oTpc= +github.com/coredns/corefile-migration v1.0.24/go.mod h1:56DPqONc3njpVPsdilEnfijCwNGC3/kTJLl7i7SPavY= 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.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -42,8 +42,8 @@ github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0 github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= -github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= -github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= @@ -83,12 +83,10 @@ github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3Bum github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= 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/gobuffalo/flect v1.0.2 h1:eqjPGSo2WmjgY2XlpGwo2NXgL3RucAKo4k4qQMNA5sA= -github.com/gobuffalo/flect v1.0.2/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= +github.com/gobuffalo/flect v1.0.3 h1:xeWBM2nui+qnVvNM4S3foBhCAL2XgPU+a7FdpelbTq4= +github.com/gobuffalo/flect v1.0.3/go.mod h1:A5msMlrHtLqh9umBSnvabjsMrCcCpAyzglnDvkbYKHs= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/cel-go v0.20.1 h1:nDx9r8S3L4pE61eDdt8igGj8rf5kjYR3ILxWIpWNi84= @@ -118,8 +116,6 @@ github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISH github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= -github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= -github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -252,8 +248,8 @@ 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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= -golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= +golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e h1:I88y4caeGeuDQxgdoFPUq097j7kNfw6uvuiNxUBfcBk= golang.org/x/exp v0.0.0-20240904232852-e7e105dedf7e/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -296,8 +292,8 @@ 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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= -golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ= +golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= @@ -315,8 +311,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 h1: google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157/go.mod h1:99sLkeliLXfdj2J75X3Ho+rrVCaJze0uwN7zDDkjPVU= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 h1:BwIjyKYGsK9dMCBOorzRri8MQwmi7mT9rGHsCEinZkA= google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094/go.mod h1:Ue6ibwXGpU+dqIcODieyLOcgj7z8+IcskoNIgZxtrFY= -google.golang.org/grpc v1.65.0 h1:bs/cUb4lp1G5iImFFd3u5ixQzweKizoZJAwBNLR42lc= -google.golang.org/grpc v1.65.0/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= +google.golang.org/grpc v1.65.1 h1:toSN4j5/Xju+HVovfaY5g1YZVuJeHzQZhP8eJ0L0f1I= +google.golang.org/grpc v1.65.1/go.mod h1:WgYC2ypjlB0EiQi6wdKixMqukr6lBc0Vo+oOgjrM5ZQ= google.golang.org/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -326,41 +322,40 @@ gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSP 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/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/api v0.31.3 h1:umzm5o8lFbdN/hIXbrK9oRpOproJO62CV1zqxXrLgk8= -k8s.io/api v0.31.3/go.mod h1:UJrkIp9pnMOI9K2nlL6vwpxRzzEX5sWgn8kGQe92kCE= -k8s.io/apiextensions-apiserver v0.31.2 h1:W8EwUb8+WXBLu56ser5IudT2cOho0gAKeTOnywBLxd0= -k8s.io/apiextensions-apiserver v0.31.2/go.mod h1:i+Geh+nGCJEGiCGR3MlBDkS7koHIIKWVfWeRFiOsUcM= -k8s.io/apimachinery v0.31.3 h1:6l0WhcYgasZ/wk9ktLq5vLaoXJJr5ts6lkaQzgeYPq4= -k8s.io/apimachinery v0.31.3/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= -k8s.io/apiserver v0.31.2 h1:VUzOEUGRCDi6kX1OyQ801m4A7AUPglpsmGvdsekmcI4= -k8s.io/apiserver v0.31.2/go.mod h1:o3nKZR7lPlJqkU5I3Ove+Zx3JuoFjQobGX1Gctw6XuE= -k8s.io/client-go v0.31.3 h1:CAlZuM+PH2cm+86LOBemaJI/lQ5linJ6UFxKX/SoG+4= -k8s.io/client-go v0.31.3/go.mod h1:2CgjPUTpv3fE5dNygAr2NcM8nhHzXvxB8KL5gYc3kJs= -k8s.io/cluster-bootstrap v0.30.3 h1:MgxyxMkpaC6mu0BKWJ8985XCOnKU+eH3Iy+biwtDXRk= -k8s.io/cluster-bootstrap v0.30.3/go.mod h1:h8BoLDfdD7XEEIXy7Bx9FcMzxHwz29jsYYi34bM5DKU= -k8s.io/component-base v0.31.2 h1:Z1J1LIaC0AV+nzcPRFqfK09af6bZ4D1nAOpWsy9owlA= -k8s.io/component-base v0.31.2/go.mod h1:9PeyyFN/drHjtJZMCTkSpQJS3U9OXORnHQqMLDz0sUQ= +k8s.io/api v0.32.0 h1:OL9JpbvAU5ny9ga2fb24X8H6xQlVp+aJMFlgtQjR9CE= +k8s.io/api v0.32.0/go.mod h1:4LEwHZEf6Q/cG96F3dqR965sYOfmPM7rq81BLgsE0p0= +k8s.io/apiextensions-apiserver v0.31.3 h1:+GFGj2qFiU7rGCsA5o+p/rul1OQIq6oYpQw4+u+nciE= +k8s.io/apiextensions-apiserver v0.31.3/go.mod h1:2DSpFhUZZJmn/cr/RweH1cEVVbzFw9YBu4T+U3mf1e4= +k8s.io/apimachinery v0.32.0 h1:cFSE7N3rmEEtv4ei5X6DaJPHHX0C+upp+v5lVPiEwpg= +k8s.io/apimachinery v0.32.0/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE= +k8s.io/apiserver v0.31.3 h1:+1oHTtCB+OheqFEz375D0IlzHZ5VeQKX1KGXnx+TTuY= +k8s.io/apiserver v0.31.3/go.mod h1:PrxVbebxrxQPFhJk4powDISIROkNMKHibTg9lTRQ0Qg= +k8s.io/client-go v0.32.0 h1:DimtMcnN/JIKZcrSrstiwvvZvLjG0aSxy8PxN8IChp8= +k8s.io/client-go v0.32.0/go.mod h1:boDWvdM1Drk4NJj/VddSLnx59X3OPgwrOo0vGbtq9+8= +k8s.io/cluster-bootstrap v0.31.3 h1:O1Yxk1bLaxZvmQCXLaJjj5iJD+lVMfJdRUuKgbUHPlA= +k8s.io/cluster-bootstrap v0.31.3/go.mod h1:TI6TCsQQB4FfcryWgNO3SLXSKWBqHjx4DfyqSFwixj8= +k8s.io/component-base v0.31.3 h1:DMCXXVx546Rfvhj+3cOm2EUxhS+EyztH423j+8sOwhQ= +k8s.io/component-base v0.31.3/go.mod h1:xME6BHfUOafRgT0rGVBGl7TuSg8Z9/deT7qq6w7qjIU= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38 h1:1dWzkmJrrprYvjGwh9kEUxmcUV/CtNU8QM7h1FLWQOo= -k8s.io/kube-openapi v0.0.0-20240903163716-9e1beecbcb38/go.mod h1:coRQXBK9NxO98XUv3ZD6AK3xzHCxV6+b7lrquKwaKzA= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f h1:GA7//TjRY9yWGy1poLzYYJJ4JRdzg3+O6e8I+e+8T5Y= +k8s.io/kube-openapi v0.0.0-20241105132330-32ad38e42d3f/go.mod h1:R/HEjbvWI0qdfb8viZUeVZm0X6IZnxAydC7YU42CMw4= k8s.io/utils v0.0.0-20241210054802-24370beab758 h1:sdbE21q2nlQtFh65saZY+rRM6x6aJJI8IUa1AmH/qa0= k8s.io/utils v0.0.0-20241210054802-24370beab758/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 h1:2770sDpzrjjsAtVhSeUFseziht227YAWYHLGNM8QPwY= sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3/go.mod h1:Ve9uj1L+deCXFrPOk1LpFXqTg7LCFzFso6PA48q/XZw= -sigs.k8s.io/cluster-api v1.8.5 h1:lNA2fPN4fkXEs+oOQlnwxT/4VwRFBpv5kkSoJG8nqBA= -sigs.k8s.io/cluster-api v1.8.5/go.mod h1:pXv5LqLxuIbhGIXykyNKiJh+KrLweSBajVHHitPLyoY= +sigs.k8s.io/cluster-api v1.9.0 h1:Iud4Zj8R/t7QX5Rvs9/V+R8HDLbf7QPVemrWfZi4g54= +sigs.k8s.io/cluster-api v1.9.0/go.mod h1:8rjpkMxLFcA87Y3P6NOi6E9RMZv2uRnN9ppOPAxrTAY= sigs.k8s.io/controller-runtime v0.19.3 h1:XO2GvC9OPftRst6xWCpTgBZO04S2cbp0Qqkj8bX1sPw= sigs.k8s.io/controller-runtime v0.19.3/go.mod h1:j4j87DqtsThvwTv5/Tc5NFRyyF/RF0ip4+62tbTSIUM= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= -sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= -sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= +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/structured-merge-diff/v4 v4.4.2 h1:MdmvkGuXi/8io6ixD5wud3vOLwc1rj0aNqRlpuvjmwA= +sigs.k8s.io/structured-merge-diff/v4 v4.4.2/go.mod h1:N8f93tFZh9U6vpxwRArLiikrE5/2tiu1w1AGfACIGE4= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= diff --git a/helm/observability-operator/Chart.yaml b/helm/observability-operator/Chart.yaml index 77c7acaa..67ddb528 100644 --- a/helm/observability-operator/Chart.yaml +++ b/helm/observability-operator/Chart.yaml @@ -3,7 +3,7 @@ name: observability-operator description: The observability-operator manages the Giant Swarm observability platform. home: https://github.com/giantswarm/observability-operator icon: https://s.giantswarm.io/app-icons/giantswarm/1/light.svg -version: 0.10.0 +version: 0.10.1 appVersion: 0.0.2 annotations: application.giantswarm.io/team: "atlas" diff --git a/internal/controller/grafanaorganization_controller.go b/internal/controller/grafanaorganization_controller.go index ca52bea4..0fa1b10f 100644 --- a/internal/controller/grafanaorganization_controller.go +++ b/internal/controller/grafanaorganization_controller.go @@ -17,10 +17,10 @@ limitations under the License. package controller import ( + "cmp" "context" "fmt" "slices" - "strings" grafanaAPI "github.com/grafana/grafana-openapi-client-go/client" "github.com/pkg/errors" @@ -138,6 +138,10 @@ func (r *GrafanaOrganizationReconciler) SetupWithManager(mgr ctrl.Manager) error return []reconcile.Request{} } + // Sort organizations by orgID to ensure the order is deterministic. + // This is important to prevent incorrect ordering of organizations on grafana restarts. + slices.SortStableFunc(organizations.Items, compareOrganizationsByID) + // Reconcile all grafana organizations when the grafana pod is recreated requests := make([]reconcile.Request, 0, len(organizations.Items)) for _, organization := range organizations.Items { @@ -154,6 +158,19 @@ func (r *GrafanaOrganizationReconciler) SetupWithManager(mgr ctrl.Manager) error Complete(r) } +func compareOrganizationsByID(i, j v1alpha1.GrafanaOrganization) int { + // if both orgs have a nil orgID, they are equal + // if one org has a nil orgID, it is higher than the other as it was not created in Grafana yet + if i.Status.OrgID == 0 && j.Status.OrgID == 0 { + return 0 + } else if i.Status.OrgID == 0 { + return 1 + } else if j.Status.OrgID == 0 { + return -1 + } + return cmp.Compare(i.Status.OrgID, j.Status.OrgID) +} + // reconcileCreate creates the grafanaOrganization. // reconcileCreate ensures the Grafana organization described in grafanaOrganization CR is created in Grafana. // This function is also responsible for: @@ -210,7 +227,7 @@ func (r GrafanaOrganizationReconciler) configureSharedOrg(ctx context.Context) e sharedOrg := grafana.SharedOrg logger.Info("configuring shared organization") - if _, err := grafana.UpdateOrganization(ctx, r.GrafanaAPI, sharedOrg); err != nil { + if err := grafana.UpdateOrganization(ctx, r.GrafanaAPI, &sharedOrg); err != nil { logger.Error(err, "failed to rename shared org") return errors.WithStack(err) } @@ -224,20 +241,28 @@ func (r GrafanaOrganizationReconciler) configureSharedOrg(ctx context.Context) e return nil } +func newOrganization(grafanaOrganization *v1alpha1.GrafanaOrganization) grafana.Organization { + tenantIDs := make([]string, len(grafanaOrganization.Spec.Tenants)) + for i, tenant := range grafanaOrganization.Spec.Tenants { + tenantIDs[i] = string(tenant) + } + + return grafana.Organization{ + ID: grafanaOrganization.Status.OrgID, + Name: grafanaOrganization.Spec.DisplayName, + TenantIDs: tenantIDs, + } +} + func (r GrafanaOrganizationReconciler) configureOrganization(ctx context.Context, grafanaOrganization *v1alpha1.GrafanaOrganization) (err error) { logger := log.FromContext(ctx) // Create or update organization in Grafana - var organization = &grafana.Organization{ - ID: grafanaOrganization.Status.OrgID, - Name: grafanaOrganization.Spec.DisplayName, - TenantID: grafanaOrganization.Name, - } - + var organization = newOrganization(grafanaOrganization) if organization.ID == 0 { // if the CR doesn't have an orgID, create the organization in Grafana - organization, err = grafana.CreateOrganization(ctx, r.GrafanaAPI, *organization) + err = grafana.CreateOrganization(ctx, r.GrafanaAPI, &organization) } else { - organization, err = grafana.UpdateOrganization(ctx, r.GrafanaAPI, *organization) + err = grafana.UpdateOrganization(ctx, r.GrafanaAPI, &organization) } if err != nil { @@ -265,11 +290,7 @@ func (r GrafanaOrganizationReconciler) configureDatasources(ctx context.Context, logger.Info("configuring data sources") // Create or update organization in Grafana - var organization = grafana.Organization{ - ID: grafanaOrganization.Status.OrgID, - Name: grafanaOrganization.Spec.DisplayName, - TenantID: grafanaOrganization.Name, - } + var organization = newOrganization(grafanaOrganization) datasources, err := grafana.ConfigureDefaultDatasources(ctx, r.GrafanaAPI, organization) if err != nil { @@ -306,11 +327,7 @@ func (r GrafanaOrganizationReconciler) reconcileDelete(ctx context.Context, graf } // Delete organization in Grafana - var organization = grafana.Organization{ - ID: grafanaOrganization.Status.OrgID, - Name: grafanaOrganization.Spec.DisplayName, - TenantID: grafanaOrganization.Name, - } + var organization = newOrganization(grafanaOrganization) // Delete organization in Grafana if it exists if grafanaOrganization.Status.OrgID > 0 { @@ -368,14 +385,11 @@ func (r *GrafanaOrganizationReconciler) configureGrafana(ctx context.Context) er } _, err = controllerutil.CreateOrPatch(ctx, r.Client, grafanaConfig, func() error { - organizations := organizationList.Items // We always sort the organizations to ensure the order is deterministic and the configmap is stable // in order to prevent grafana to restarts. - slices.SortFunc(organizations, func(i, j v1alpha1.GrafanaOrganization) int { - return strings.Compare(i.Name, j.Name) - }) + slices.SortStableFunc(organizationList.Items, compareOrganizationsByID) - config, err := templating.GenerateGrafanaConfiguration(organizations) + config, err := templating.GenerateGrafanaConfiguration(organizationList.Items) if err != nil { logger.Error(err, "failed to generate grafana user configmap values.") return errors.WithStack(err) @@ -383,7 +397,7 @@ func (r *GrafanaOrganizationReconciler) configureGrafana(ctx context.Context) er // TODO: to be removed for next release // cleanup owner references from the config map, see https://github.com/giantswarm/observability-operator/pull/183 - for _, organization := range organizations { + for _, organization := range organizationList.Items { // nolint:errcheck,gosec // ignore errors, owner references are probably already gone controllerutil.RemoveOwnerReference(&organization, grafanaConfig, r.Scheme) } diff --git a/internal/controller/predicates/predicates_test.go b/internal/controller/predicates/predicates_test.go new file mode 100644 index 00000000..44c6ec44 --- /dev/null +++ b/internal/controller/predicates/predicates_test.go @@ -0,0 +1,210 @@ +package predicates + +import ( + "testing" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/event" +) + +func TestIsGrafanaPod(t *testing.T) { + tests := []struct { + name string + pod *corev1.Pod + expected bool + }{ + { + name: "nil pod", + pod: nil, + expected: false, + }, + { + name: "non-Grafana pod", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "non-grafana-pod", + Namespace: "default", + }, + }, + expected: false, + }, + { + name: "Grafana pod with correct labels", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grafana-pod", + Namespace: "monitoring", + Labels: map[string]string{ + "app.kubernetes.io/instance": "grafana", + }, + }, + }, + expected: true, + }, + { + name: "Grafana pod with incorrect namespace", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grafana-pod", + Namespace: "default", + Labels: map[string]string{ + "app.kubernetes.io/instance": "grafana", + }, + }, + }, + expected: false, + }, + { + name: "Grafana pod with incorrect label", + pod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grafana-pod", + Namespace: "monitoring", + Labels: map[string]string{ + "app.kubernetes.io/instance": "not-grafana", + }, + }, + }, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isGrafanaPod(tt.pod) + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} + +func TestGrafanaPodRecreatedPredicate_Update(t *testing.T) { + tests := []struct { + name string + oldPod *corev1.Pod + newPod *corev1.Pod + expected bool + }{ + { + name: "nil old object", + oldPod: nil, + newPod: &corev1.Pod{}, + expected: false, + }, + { + name: "nil new object", + oldPod: &corev1.Pod{}, + newPod: nil, + expected: false, + }, + { + name: "non-Grafana pod", + oldPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "non-grafana-pod", + Namespace: "default", + }, + }, + newPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "non-grafana-pod", + Namespace: "default", + }, + }, + expected: false, + }, + { + name: "Grafana pod not ready to ready", + oldPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grafana-pod", + Namespace: "monitoring", + Labels: map[string]string{ + "app.kubernetes.io/instance": "grafana", + }, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionFalse, + }, + }, + }, + }, + newPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grafana-pod", + Namespace: "monitoring", + Labels: map[string]string{ + "app.kubernetes.io/instance": "grafana", + }, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + expected: true, + }, + { + name: "Grafana pod ready to not ready", + oldPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grafana-pod", + Namespace: "monitoring", + Labels: map[string]string{ + "app.kubernetes.io/instance": "grafana", + }, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionTrue, + }, + }, + }, + }, + newPod: &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: "grafana-pod", + Namespace: "monitoring", + Labels: map[string]string{ + "app.kubernetes.io/instance": "grafana", + }, + }, + Status: corev1.PodStatus{ + Conditions: []corev1.PodCondition{ + { + Type: corev1.PodReady, + Status: corev1.ConditionFalse, + }, + }, + }, + }, + expected: false, + }, + } + + predicate := GrafanaPodRecreatedPredicate{} + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := event.UpdateEvent{ + ObjectOld: tt.oldPod, + ObjectNew: tt.newPod, + } + result := predicate.Update(e) + if result != tt.expected { + t.Errorf("expected %v, got %v", tt.expected, result) + } + }) + } +} diff --git a/pkg/grafana/grafana.go b/pkg/grafana/grafana.go index 3c7ffe11..c1a39213 100644 --- a/pkg/grafana/grafana.go +++ b/pkg/grafana/grafana.go @@ -18,9 +18,9 @@ const ( ) var SharedOrg = Organization{ - ID: 1, - Name: "Shared Org", - TenantID: "giantswarm", + ID: 1, + Name: "Shared Org", + TenantIDs: []string{"giantswarm"}, } // We need to use a custom name for now until we can replace the existing datasources. @@ -69,13 +69,13 @@ var defaultDatasources = []Datasource{ }, } -func CreateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization Organization) (*Organization, error) { +func CreateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization *Organization) error { logger := log.FromContext(ctx) logger.Info("creating organization") err := assertNameIsAvailable(ctx, grafanaAPI, organization) if err != nil { - return nil, errors.WithStack(err) + return errors.WithStack(err) } createdOrg, err := grafanaAPI.Orgs.CreateOrg(&models.CreateOrgCommand{ @@ -83,15 +83,15 @@ func CreateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, }) if err != nil { logger.Error(err, "failed to create organization") - return nil, errors.WithStack(err) + return errors.WithStack(err) } logger.Info("created organization") organization.ID = *createdOrg.Payload.OrgID - return &organization, nil + return nil } -func UpdateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization Organization) (*Organization, error) { +func UpdateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization *Organization) error { logger := log.FromContext(ctx) logger.Info("updating organization") @@ -103,18 +103,18 @@ func UpdateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, return CreateOrganization(ctx, grafanaAPI, organization) } logger.Error(err, fmt.Sprintf("failed to find organization with ID: %d", organization.ID)) - return nil, errors.WithStack(err) + return errors.WithStack(err) } // If both name matches, there is nothing to do. if found.Name == organization.Name { logger.Info("the organization already exists in Grafana and does not need to be updated.") - return &organization, nil + return nil } err = assertNameIsAvailable(ctx, grafanaAPI, organization) if err != nil { - return nil, errors.WithStack(err) + return errors.WithStack(err) } // if the name of the CR is different from the name of the org in Grafana, update the name of the org in Grafana using the CR's display name. @@ -123,12 +123,12 @@ func UpdateOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, }) if err != nil { logger.Error(err, "failed to update organization name") - return nil, errors.WithStack(err) + return errors.WithStack(err) } logger.Info("updated organization") - return &organization, nil + return nil } func DeleteOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization Organization) error { @@ -158,6 +158,7 @@ func DeleteOrganization(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, func ConfigureDefaultDatasources(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization Organization) ([]Datasource, error) { logger := log.FromContext(ctx) + // TODO using a serviceaccount later would be better as they are scoped to an organization var err error @@ -278,7 +279,7 @@ func isNotFound(err error) bool { } // assertNameIsAvailable is a helper function to check if the organization name is available in Grafana -func assertNameIsAvailable(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization Organization) error { +func assertNameIsAvailable(ctx context.Context, grafanaAPI *client.GrafanaHTTPAPI, organization *Organization) error { logger := log.FromContext(ctx) found, err := findByName(grafanaAPI, organization.Name) diff --git a/pkg/grafana/types.go b/pkg/grafana/types.go index 595bbea0..6ded75be 100644 --- a/pkg/grafana/types.go +++ b/pkg/grafana/types.go @@ -1,9 +1,11 @@ package grafana +import "strings" + type Organization struct { - ID int64 - Name string - TenantID string + ID int64 + Name string + TenantIDs []string } type Datasource struct { @@ -33,12 +35,12 @@ func (d Datasource) buildJSONData() map[string]interface{} { } func (d Datasource) buildSecureJSONData(organization Organization) map[string]string { - tenant := organization.TenantID + tenantIDs := organization.TenantIDs if d.Type != "loki" { // We do not support multi-tenancy for Mimir yet - tenant = "anonymous" + tenantIDs = []string{"anonymous"} } return map[string]string{ - "httpHeaderValue1": tenant, + "httpHeaderValue1": strings.Join(tenantIDs, "|"), } }