mirror of
https://github.com/cloudnative-pg/plugin-barman-cloud.git
synced 2026-07-10 03:32:21 +02:00
Compare commits
7 Commits
551129ad78
...
d0032a5600
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0032a5600 | ||
|
|
f41c034877 | ||
|
|
441f43be24 | ||
|
|
e72c779a29 | ||
|
|
ddb222a30f | ||
|
|
8971a397f4 | ||
|
|
b1f373d84b |
@ -22,13 +22,13 @@ barman==3.18.0 \
|
|||||||
--hash=sha256:8e752ac93d2f3a61e86b8374185209cae477a638ece7e6f540070f36d28d6997 \
|
--hash=sha256:8e752ac93d2f3a61e86b8374185209cae477a638ece7e6f540070f36d28d6997 \
|
||||||
--hash=sha256:ff90c44dafa4107b7574142771cdc2611c4cf1af818d93d3e67440a0c81164b9
|
--hash=sha256:ff90c44dafa4107b7574142771cdc2611c4cf1af818d93d3e67440a0c81164b9
|
||||||
# via -r sidecar-requirements.in
|
# via -r sidecar-requirements.in
|
||||||
boto3==1.42.86 \
|
boto3==1.42.88 \
|
||||||
--hash=sha256:492c3c7cbbe9842882680064902f50cf711b5ab770d26525549872339ed95d5b \
|
--hash=sha256:2d0f52c971503377e4370d2a83edee6f077ddb8e684366ff38df4f13581d9cfc \
|
||||||
--hash=sha256:c87d2a750b1a8cad0384d1a83d3bad6aedf924ae9a14aaba814bcb3297b39c01
|
--hash=sha256:2d22c70de5726918676a06f1a03acfb4d5d9ea92fc759354800b67b22aaeef19
|
||||||
# via barman
|
# via barman
|
||||||
botocore==1.42.86 \
|
botocore==1.42.88 \
|
||||||
--hash=sha256:443387337864e069f7e4e885ccdc81592725b5598ca966514af3e9776bce0bfe \
|
--hash=sha256:032375b213305b6b81eedb269eaeefdf96f674620799bbf96117dca86052cc1a \
|
||||||
--hash=sha256:baa49e93b4c92d63e0c8288026ee1ef8de83f182743127cc9175504440a48e49
|
--hash=sha256:cbb59ee464662039b0c2c95a520cdf85b1e8ce00b72375ab9cd9f842cc001301
|
||||||
# via
|
# via
|
||||||
# boto3
|
# boto3
|
||||||
# s3transfer
|
# s3transfer
|
||||||
@ -446,15 +446,15 @@ cryptography==46.0.7 \
|
|||||||
# google-auth
|
# google-auth
|
||||||
# msal
|
# msal
|
||||||
# pyjwt
|
# pyjwt
|
||||||
google-api-core==2.30.2 \
|
google-api-core==2.30.3 \
|
||||||
--hash=sha256:9a8113e1a88bdc09a7ff629707f2214d98d61c7f6ceb0ea38c42a095d02dc0f9 \
|
--hash=sha256:a85761ba72c444dad5d611c2220633480b2b6be2521eca69cca2dbb3ffd6bfe8 \
|
||||||
--hash=sha256:a4c226766d6af2580577db1f1a51bf53cd262f722b49731ce7414c43068a9594
|
--hash=sha256:e601a37f148585319b26db36e219df68c5d07b6382cff2d580e83404e44d641b
|
||||||
# via
|
# via
|
||||||
# google-cloud-core
|
# google-cloud-core
|
||||||
# google-cloud-storage
|
# google-cloud-storage
|
||||||
google-auth==2.49.1 \
|
google-auth==2.49.2 \
|
||||||
--hash=sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64 \
|
--hash=sha256:c1ae38500e73065dcae57355adb6278cf8b5c8e391994ae9cbadbcb9631ab409 \
|
||||||
--hash=sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7
|
--hash=sha256:c2720924dfc82dedb962c9f52cabb2ab16714fd0a6a707e40561d217574ed6d5
|
||||||
# via
|
# via
|
||||||
# google-api-core
|
# google-api-core
|
||||||
# google-cloud-core
|
# google-cloud-core
|
||||||
|
|||||||
10
go.mod
10
go.mod
@ -5,7 +5,7 @@ go 1.25.0
|
|||||||
toolchain go1.26.2
|
toolchain go1.26.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/cert-manager/cert-manager v1.20.1
|
github.com/cert-manager/cert-manager v1.20.2
|
||||||
github.com/cloudnative-pg/api v1.29.0
|
github.com/cloudnative-pg/api v1.29.0
|
||||||
github.com/cloudnative-pg/barman-cloud v0.5.0
|
github.com/cloudnative-pg/barman-cloud v0.5.0
|
||||||
github.com/cloudnative-pg/cloudnative-pg v1.29.0
|
github.com/cloudnative-pg/cloudnative-pg v1.29.0
|
||||||
@ -102,12 +102,12 @@ require (
|
|||||||
github.com/xlab/treeprint v1.2.0 // indirect
|
github.com/xlab/treeprint v1.2.0 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.7.0 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
go.uber.org/zap v1.27.1 // indirect
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
|
|||||||
24
go.sum
24
go.sum
@ -14,8 +14,8 @@ 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/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
||||||
github.com/cert-manager/cert-manager v1.20.1 h1:99ExHJu5TPp1V92AvvE4oY6BkOSyJiWLxxMkbqbdGaY=
|
github.com/cert-manager/cert-manager v1.20.2 h1:CimnY00nLqB2lmxhoSuEC4GDMFDK7JCXqyjwMM9ndIQ=
|
||||||
github.com/cert-manager/cert-manager v1.20.1/go.mod h1:ut67FnggYJJqAdDWLhSPnj10P06QwbNU88RYNh9MvMc=
|
github.com/cert-manager/cert-manager v1.20.2/go.mod h1:1g/+a/WK5zWH/dXPZa3dMD3aJQJNRXQu+PN17C6WrOw=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
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/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudnative-pg/api v1.29.0 h1:mNx6yJ5qi+Xrjs0NYrUy6V4MlXBkVJxGKwvTJZIuTX4=
|
github.com/cloudnative-pg/api v1.29.0 h1:mNx6yJ5qi+Xrjs0NYrUy6V4MlXBkVJxGKwvTJZIuTX4=
|
||||||
@ -243,20 +243,20 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
|
|||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
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 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0 h1:Ahq7pZmv87yiyn3jeFz/LekZmPLLdKejuO3NcK9MssM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.37.0/go.mod h1:MJTqhM0im3mRLw1i8uGHnCvUEeS7VwRyxlLC78PA18M=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0 h1:EtFWSnwW9hGObjkIdmlnWSydO+Qs8OwzfzXLUPg4xOc=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.37.0/go.mod h1:QjUEoiGCPkvFZ/MjK6ZZfNOS6mfVEVKYE99dFhuN2LI=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
go.opentelemetry.io/proto/otlp v1.7.0 h1:jX1VolD6nHuFzOYso2E73H85i92Mv8JQYk0K9vz09os=
|
||||||
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
go.opentelemetry.io/proto/otlp v1.7.0/go.mod h1:fSKjH6YJ7HDlwzltzyMj036AJ3ejJLCgCSHGj4efDDo=
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
|||||||
@ -57,6 +57,8 @@ func NewCmd() *cobra.Command {
|
|||||||
_ = viper.BindEnv("pod-name", "POD_NAME")
|
_ = viper.BindEnv("pod-name", "POD_NAME")
|
||||||
_ = viper.BindEnv("pgdata", "PGDATA")
|
_ = viper.BindEnv("pgdata", "PGDATA")
|
||||||
_ = viper.BindEnv("spool-directory", "SPOOL_DIRECTORY")
|
_ = viper.BindEnv("spool-directory", "SPOOL_DIRECTORY")
|
||||||
|
_ = viper.BindEnv("custom-cnpg-group", "CUSTOM_CNPG_GROUP")
|
||||||
|
_ = viper.BindEnv("custom-cnpg-version", "CUSTOM_CNPG_VERSION")
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
}
|
}
|
||||||
|
|||||||
67
internal/cnpgi/common/scheme.go
Normal file
67
internal/cnpgi/common/scheme.go
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||||
|
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||||
|
|
||||||
|
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GenerateScheme creates a runtime.Scheme object with all the
|
||||||
|
// definition needed to support the sidecar. This allows
|
||||||
|
// the plugin to be used in every CNPG-based operator.
|
||||||
|
func GenerateScheme(ctx context.Context) *runtime.Scheme {
|
||||||
|
result := runtime.NewScheme()
|
||||||
|
|
||||||
|
utilruntime.Must(barmancloudv1.AddToScheme(result))
|
||||||
|
utilruntime.Must(clientgoscheme.AddToScheme(result))
|
||||||
|
|
||||||
|
cnpgGroup := viper.GetString("custom-cnpg-group")
|
||||||
|
cnpgVersion := viper.GetString("custom-cnpg-version")
|
||||||
|
if len(cnpgGroup) == 0 {
|
||||||
|
cnpgGroup = cnpgv1.SchemeGroupVersion.Group
|
||||||
|
}
|
||||||
|
if len(cnpgVersion) == 0 {
|
||||||
|
cnpgVersion = cnpgv1.SchemeGroupVersion.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
// Proceed with custom registration of the CNPG scheme
|
||||||
|
schemeGroupVersion := schema.GroupVersion{Group: cnpgGroup, Version: cnpgVersion}
|
||||||
|
schemeBuilder := &scheme.Builder{GroupVersion: schemeGroupVersion}
|
||||||
|
schemeBuilder.Register(&cnpgv1.Cluster{}, &cnpgv1.ClusterList{})
|
||||||
|
schemeBuilder.Register(&cnpgv1.Backup{}, &cnpgv1.BackupList{})
|
||||||
|
schemeBuilder.Register(&cnpgv1.ScheduledBackup{}, &cnpgv1.ScheduledBackupList{})
|
||||||
|
utilruntime.Must(schemeBuilder.AddToScheme(result))
|
||||||
|
|
||||||
|
schemeLog := log.FromContext(ctx)
|
||||||
|
schemeLog.Info("CNPG types registration", "schemeGroupVersion", schemeGroupVersion)
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
@ -27,22 +27,18 @@ import (
|
|||||||
"github.com/cloudnative-pg/machinery/pkg/log"
|
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
||||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/scheme"
|
|
||||||
|
|
||||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/common"
|
||||||
extendedclient "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/instance/internal/client"
|
extendedclient "github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/instance/internal/client"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Start starts the sidecar informers and CNPG-i server
|
// Start starts the sidecar informers and CNPG-i server
|
||||||
func Start(ctx context.Context) error {
|
func Start(ctx context.Context) error {
|
||||||
scheme := generateScheme(ctx)
|
scheme := common.GenerateScheme(ctx)
|
||||||
|
|
||||||
setupLog := log.FromContext(ctx)
|
setupLog := log.FromContext(ctx)
|
||||||
setupLog.Info("Starting barman cloud instance plugin")
|
setupLog.Info("Starting barman cloud instance plugin")
|
||||||
@ -118,35 +114,3 @@ func Start(ctx context.Context) error {
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// generateScheme creates a runtime.Scheme object with all the
|
|
||||||
// definition needed to support the sidecar. This allows
|
|
||||||
// the plugin to be used in every CNPG-based operator.
|
|
||||||
func generateScheme(ctx context.Context) *runtime.Scheme {
|
|
||||||
result := runtime.NewScheme()
|
|
||||||
|
|
||||||
utilruntime.Must(barmancloudv1.AddToScheme(result))
|
|
||||||
utilruntime.Must(clientgoscheme.AddToScheme(result))
|
|
||||||
|
|
||||||
cnpgGroup := viper.GetString("custom-cnpg-group")
|
|
||||||
cnpgVersion := viper.GetString("custom-cnpg-version")
|
|
||||||
if len(cnpgGroup) == 0 {
|
|
||||||
cnpgGroup = cnpgv1.SchemeGroupVersion.Group
|
|
||||||
}
|
|
||||||
if len(cnpgVersion) == 0 {
|
|
||||||
cnpgVersion = cnpgv1.SchemeGroupVersion.Version
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proceed with custom registration of the CNPG scheme
|
|
||||||
schemeGroupVersion := schema.GroupVersion{Group: cnpgGroup, Version: cnpgVersion}
|
|
||||||
schemeBuilder := &scheme.Builder{GroupVersion: schemeGroupVersion}
|
|
||||||
schemeBuilder.Register(&cnpgv1.Cluster{}, &cnpgv1.ClusterList{})
|
|
||||||
schemeBuilder.Register(&cnpgv1.Backup{}, &cnpgv1.BackupList{})
|
|
||||||
schemeBuilder.Register(&cnpgv1.ScheduledBackup{}, &cnpgv1.ScheduledBackupList{})
|
|
||||||
utilruntime.Must(schemeBuilder.AddToScheme(result))
|
|
||||||
|
|
||||||
schemeLog := log.FromContext(ctx)
|
|
||||||
schemeLog.Info("CNPG types registration", "schemeGroupVersion", schemeGroupVersion)
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|||||||
@ -26,6 +26,10 @@ import "github.com/cloudnative-pg/cnpg-i/pkg/identity"
|
|||||||
const PluginName = "barman-cloud.cloudnative-pg.io"
|
const PluginName = "barman-cloud.cloudnative-pg.io"
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// ClusterLabelName is the label applied to RBAC resources created
|
||||||
|
// by this plugin. Its value is the name of the owning Cluster.
|
||||||
|
ClusterLabelName = "barmancloud.cnpg.io/cluster"
|
||||||
|
|
||||||
// CheckEmptyWalArchiveFile is the name of the file in the PGDATA that,
|
// CheckEmptyWalArchiveFile is the name of the file in the PGDATA that,
|
||||||
// if present, requires the WAL archiver to check that the backup object
|
// if present, requires the WAL archiver to check that the backup object
|
||||||
// store is empty.
|
// store is empty.
|
||||||
|
|||||||
@ -24,7 +24,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
|
||||||
// +kubebuilder:scaffold:imports
|
// +kubebuilder:scaffold:imports
|
||||||
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
|
||||||
"github.com/cloudnative-pg/machinery/pkg/log"
|
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@ -49,7 +48,6 @@ var scheme = runtime.NewScheme()
|
|||||||
func init() {
|
func init() {
|
||||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
||||||
utilruntime.Must(barmancloudv1.AddToScheme(scheme))
|
utilruntime.Must(barmancloudv1.AddToScheme(scheme))
|
||||||
utilruntime.Must(cnpgv1.AddToScheme(scheme))
|
|
||||||
// +kubebuilder:scaffold:scheme
|
// +kubebuilder:scaffold:scheme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
22
internal/cnpgi/operator/rbac/doc.go
Normal file
22
internal/cnpgi/operator/rbac/doc.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package rbac contains utilities to reconcile RBAC resources
|
||||||
|
// for the barman-cloud plugin.
|
||||||
|
package rbac
|
||||||
181
internal/cnpgi/operator/rbac/ensure.go
Normal file
181
internal/cnpgi/operator/rbac/ensure.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package rbac
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||||
|
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/api/equality"
|
||||||
|
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
"k8s.io/client-go/util/retry"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EnsureRole ensures the RBAC Role for the given Cluster matches
|
||||||
|
// the desired state derived from the given ObjectStores. On creation,
|
||||||
|
// the Cluster is set as the owner of the Role for garbage collection.
|
||||||
|
//
|
||||||
|
// This function is called from the Pre hook (gRPC). It creates the
|
||||||
|
// Role if it does not exist, then patches rules and labels to match
|
||||||
|
// the desired state.
|
||||||
|
//
|
||||||
|
// Note: the ObjectStore controller (EnsureRoleRules) can patch the
|
||||||
|
// same Role concurrently. Both paths use RetryOnConflict but compute
|
||||||
|
// desired rules from their own view of ObjectStores. If the Pre hook
|
||||||
|
// reads stale ObjectStore data from the informer cache, it may
|
||||||
|
// briefly revert a fresher update. This is self-healing: the next
|
||||||
|
// ObjectStore reconcile restores the correct state.
|
||||||
|
func EnsureRole(
|
||||||
|
ctx context.Context,
|
||||||
|
c client.Client,
|
||||||
|
cluster *cnpgv1.Cluster,
|
||||||
|
barmanObjects []barmancloudv1.ObjectStore,
|
||||||
|
) error {
|
||||||
|
newRole := specs.BuildRole(cluster, barmanObjects)
|
||||||
|
roleKey := client.ObjectKeyFromObject(newRole)
|
||||||
|
|
||||||
|
if err := ensureRoleExists(ctx, c, cluster, newRole); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return patchRole(ctx, c, roleKey, newRole.Rules, map[string]string{
|
||||||
|
metadata.ClusterLabelName: cluster.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnsureRoleRules updates the rules of an existing Role to match
|
||||||
|
// the desired state derived from the given ObjectStores. Unlike
|
||||||
|
// EnsureRole, this function does not create Roles or set owner
|
||||||
|
// references — it only patches rules on Roles that already exist.
|
||||||
|
// It is intended for the ObjectStore controller path where no
|
||||||
|
// Cluster object is available. Returns nil if the Role does not
|
||||||
|
// exist (the Pre hook has not created it yet).
|
||||||
|
func EnsureRoleRules(
|
||||||
|
ctx context.Context,
|
||||||
|
c client.Client,
|
||||||
|
roleKey client.ObjectKey,
|
||||||
|
barmanObjects []barmancloudv1.ObjectStore,
|
||||||
|
) error {
|
||||||
|
err := patchRole(ctx, c, roleKey, specs.BuildRoleRules(barmanObjects), nil)
|
||||||
|
if apierrs.IsNotFound(err) {
|
||||||
|
log.FromContext(ctx).Debug("Role not found, skipping rule update",
|
||||||
|
"name", roleKey.Name, "namespace", roleKey.Namespace)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureRoleExists creates the Role if it does not exist. Returns
|
||||||
|
// nil on success and nil on AlreadyExists (another writer created
|
||||||
|
// it concurrently). The caller always follows up with patchRole.
|
||||||
|
func ensureRoleExists(
|
||||||
|
ctx context.Context,
|
||||||
|
c client.Client,
|
||||||
|
cluster *cnpgv1.Cluster,
|
||||||
|
newRole *rbacv1.Role,
|
||||||
|
) error {
|
||||||
|
contextLogger := log.FromContext(ctx)
|
||||||
|
|
||||||
|
var existing rbacv1.Role
|
||||||
|
err := c.Get(ctx, client.ObjectKeyFromObject(newRole), &existing)
|
||||||
|
if err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !apierrs.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := specs.SetControllerReference(cluster, newRole); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
contextLogger.Info("Creating role",
|
||||||
|
"name", newRole.Name, "namespace", newRole.Namespace)
|
||||||
|
|
||||||
|
createErr := c.Create(ctx, newRole)
|
||||||
|
if createErr == nil || apierrs.IsAlreadyExists(createErr) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return createErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// patchRole patches the Role's rules and optionally its labels to
|
||||||
|
// match the desired state. When desiredLabels is nil, labels are
|
||||||
|
// not modified. Uses retry.RetryOnConflict for concurrent
|
||||||
|
// modification handling.
|
||||||
|
func patchRole(
|
||||||
|
ctx context.Context,
|
||||||
|
c client.Client,
|
||||||
|
roleKey client.ObjectKey,
|
||||||
|
desiredRules []rbacv1.PolicyRule,
|
||||||
|
desiredLabels map[string]string,
|
||||||
|
) error {
|
||||||
|
return retry.RetryOnConflict(retry.DefaultBackoff, func() error {
|
||||||
|
var role rbacv1.Role
|
||||||
|
if err := c.Get(ctx, roleKey, &role); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
rulesMatch := equality.Semantic.DeepEqual(desiredRules, role.Rules)
|
||||||
|
labelsMatch := desiredLabels == nil || !labelsNeedUpdate(role.Labels, desiredLabels)
|
||||||
|
|
||||||
|
if rulesMatch && labelsMatch {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
contextLogger := log.FromContext(ctx)
|
||||||
|
contextLogger.Info("Patching role",
|
||||||
|
"name", role.Name, "namespace", role.Namespace)
|
||||||
|
|
||||||
|
oldRole := role.DeepCopy()
|
||||||
|
role.Rules = desiredRules
|
||||||
|
|
||||||
|
if desiredLabels != nil {
|
||||||
|
if role.Labels == nil {
|
||||||
|
role.Labels = make(map[string]string, len(desiredLabels))
|
||||||
|
}
|
||||||
|
for k, v := range desiredLabels {
|
||||||
|
role.Labels[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Patch(ctx, &role, client.MergeFrom(oldRole))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// labelsNeedUpdate returns true if any key in desired is missing
|
||||||
|
// or has a different value in existing.
|
||||||
|
func labelsNeedUpdate(existing, desired map[string]string) bool {
|
||||||
|
for k, v := range desired {
|
||||||
|
if existing[k] != v {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
359
internal/cnpgi/operator/rbac/ensure_test.go
Normal file
359
internal/cnpgi/operator/rbac/ensure_test.go
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package rbac_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
|
||||||
|
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||||
|
machineryapi "github.com/cloudnative-pg/machinery/pkg/api"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
|
||||||
|
|
||||||
|
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/rbac"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newScheme() *runtime.Scheme {
|
||||||
|
s := runtime.NewScheme()
|
||||||
|
utilruntime.Must(rbacv1.AddToScheme(s))
|
||||||
|
utilruntime.Must(cnpgv1.AddToScheme(s))
|
||||||
|
utilruntime.Must(barmancloudv1.AddToScheme(s))
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func newCluster(name, namespace string) *cnpgv1.Cluster {
|
||||||
|
return &cnpgv1.Cluster{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
APIVersion: cnpgv1.SchemeGroupVersion.String(),
|
||||||
|
Kind: cnpgv1.ClusterKind,
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newObjectStore(name, namespace, secretName string) barmancloudv1.ObjectStore {
|
||||||
|
return barmancloudv1.ObjectStore{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Spec: barmancloudv1.ObjectStoreSpec{
|
||||||
|
Configuration: barmanapi.BarmanObjectStoreConfiguration{
|
||||||
|
DestinationPath: "s3://bucket/path",
|
||||||
|
BarmanCredentials: barmanapi.BarmanCredentials{
|
||||||
|
AWS: &barmanapi.S3Credentials{
|
||||||
|
AccessKeyIDReference: &machineryapi.SecretKeySelector{
|
||||||
|
LocalObjectReference: machineryapi.LocalObjectReference{
|
||||||
|
Name: secretName,
|
||||||
|
},
|
||||||
|
Key: "ACCESS_KEY_ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Describe("EnsureRole", func() {
|
||||||
|
var (
|
||||||
|
ctx context.Context
|
||||||
|
cluster *cnpgv1.Cluster
|
||||||
|
objects []barmancloudv1.ObjectStore
|
||||||
|
fakeClient client.Client
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx = context.Background()
|
||||||
|
cluster = newCluster("test-cluster", "default")
|
||||||
|
objects = []barmancloudv1.ObjectStore{
|
||||||
|
newObjectStore("my-store", "default", "aws-creds"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when the Role does not exist", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should create the Role with owner reference and label", func() {
|
||||||
|
err := rbac.EnsureRole(ctx, fakeClient, cluster, objects)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var role rbacv1.Role
|
||||||
|
err = fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-cluster-barman-cloud",
|
||||||
|
}, &role)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(role.Rules).To(HaveLen(3))
|
||||||
|
|
||||||
|
Expect(role.OwnerReferences).To(HaveLen(1))
|
||||||
|
Expect(role.OwnerReferences[0].Name).To(Equal("test-cluster"))
|
||||||
|
Expect(role.OwnerReferences[0].Kind).To(Equal("Cluster"))
|
||||||
|
|
||||||
|
Expect(role.Labels).To(HaveKeyWithValue(metadata.ClusterLabelName, "test-cluster"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when the Role exists with matching rules", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()
|
||||||
|
Expect(rbac.EnsureRole(ctx, fakeClient, cluster, objects)).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should not patch the Role", func() {
|
||||||
|
var before rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-cluster-barman-cloud",
|
||||||
|
}, &before)).To(Succeed())
|
||||||
|
|
||||||
|
err := rbac.EnsureRole(ctx, fakeClient, cluster, objects)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var after rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-cluster-barman-cloud",
|
||||||
|
}, &after)).To(Succeed())
|
||||||
|
|
||||||
|
Expect(after.ResourceVersion).To(Equal(before.ResourceVersion))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when the Role exists with different rules", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()
|
||||||
|
oldObjects := []barmancloudv1.ObjectStore{
|
||||||
|
newObjectStore("my-store", "default", "old-secret"),
|
||||||
|
}
|
||||||
|
Expect(rbac.EnsureRole(ctx, fakeClient, cluster, oldObjects)).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should patch the Role with new rules and preserve owner reference", func() {
|
||||||
|
err := rbac.EnsureRole(ctx, fakeClient, cluster, objects)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var role rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-cluster-barman-cloud",
|
||||||
|
}, &role)).To(Succeed())
|
||||||
|
|
||||||
|
secretsRule := role.Rules[2]
|
||||||
|
Expect(secretsRule.ResourceNames).To(ContainElement("aws-creds"))
|
||||||
|
Expect(secretsRule.ResourceNames).NotTo(ContainElement("old-secret"))
|
||||||
|
|
||||||
|
Expect(role.OwnerReferences).To(HaveLen(1))
|
||||||
|
Expect(role.OwnerReferences[0].Name).To(Equal("test-cluster"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when Role creation fails with a transient error", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
internalErr := apierrs.NewInternalError(fmt.Errorf("etcd timeout"))
|
||||||
|
fakeClient = fake.NewClientBuilder().
|
||||||
|
WithScheme(newScheme()).
|
||||||
|
WithInterceptorFuncs(interceptor.Funcs{
|
||||||
|
Create: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
|
||||||
|
return internalErr
|
||||||
|
},
|
||||||
|
}).
|
||||||
|
Build()
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should propagate the error", func() {
|
||||||
|
err := rbac.EnsureRole(ctx, fakeClient, cluster, objects)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(apierrs.IsInternalError(err)).To(BeTrue())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when the Role has pre-existing unrelated labels", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()
|
||||||
|
existing := &rbacv1.Role{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-cluster-barman-cloud",
|
||||||
|
Namespace: "default",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"app.kubernetes.io/managed-by": "helm",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(fakeClient.Create(ctx, existing)).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should preserve unrelated labels while adding the cluster label", func() {
|
||||||
|
err := rbac.EnsureRole(ctx, fakeClient, cluster, objects)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var role rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-cluster-barman-cloud",
|
||||||
|
}, &role)).To(Succeed())
|
||||||
|
|
||||||
|
Expect(role.Labels).To(HaveKeyWithValue("app.kubernetes.io/managed-by", "helm"))
|
||||||
|
Expect(role.Labels).To(HaveKeyWithValue(metadata.ClusterLabelName, "test-cluster"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when the Role exists without the cluster label (upgrade scenario)", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()
|
||||||
|
|
||||||
|
// Create a Role without the label (simulates pre-upgrade state)
|
||||||
|
unlabeledRole := &rbacv1.Role{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-cluster-barman-cloud",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Rules: []rbacv1.PolicyRule{},
|
||||||
|
}
|
||||||
|
Expect(fakeClient.Create(ctx, unlabeledRole)).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should add the label and update rules", func() {
|
||||||
|
err := rbac.EnsureRole(ctx, fakeClient, cluster, objects)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var role rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-cluster-barman-cloud",
|
||||||
|
}, &role)).To(Succeed())
|
||||||
|
|
||||||
|
Expect(role.Labels).To(HaveKeyWithValue(metadata.ClusterLabelName, "test-cluster"))
|
||||||
|
Expect(role.Rules).To(HaveLen(3))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
var _ = Describe("EnsureRoleRules", func() {
|
||||||
|
var (
|
||||||
|
ctx context.Context
|
||||||
|
fakeClient client.Client
|
||||||
|
objects []barmancloudv1.ObjectStore
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
ctx = context.Background()
|
||||||
|
objects = []barmancloudv1.ObjectStore{
|
||||||
|
newObjectStore("my-store", "default", "aws-creds"),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when the Role exists", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()
|
||||||
|
|
||||||
|
// Seed a labeled Role with old rules
|
||||||
|
cluster := newCluster("test-cluster", "default")
|
||||||
|
oldObjects := []barmancloudv1.ObjectStore{
|
||||||
|
newObjectStore("my-store", "default", "old-secret"),
|
||||||
|
}
|
||||||
|
Expect(rbac.EnsureRole(ctx, fakeClient, cluster, oldObjects)).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should patch the rules", func() {
|
||||||
|
roleKey := client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-cluster-barman-cloud",
|
||||||
|
}
|
||||||
|
err := rbac.EnsureRoleRules(ctx, fakeClient, roleKey, objects)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var role rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, roleKey, &role)).To(Succeed())
|
||||||
|
|
||||||
|
secretsRule := role.Rules[2]
|
||||||
|
Expect(secretsRule.ResourceNames).To(ContainElement("aws-creds"))
|
||||||
|
Expect(secretsRule.ResourceNames).NotTo(ContainElement("old-secret"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should not patch when rules already match", func() {
|
||||||
|
// Seed with the same objects so rules match
|
||||||
|
cluster := newCluster("test-cluster", "default")
|
||||||
|
Expect(rbac.EnsureRole(ctx, fakeClient, cluster, objects)).To(Succeed())
|
||||||
|
|
||||||
|
roleKey := client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-cluster-barman-cloud",
|
||||||
|
}
|
||||||
|
|
||||||
|
var before rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, roleKey, &before)).To(Succeed())
|
||||||
|
|
||||||
|
Expect(rbac.EnsureRoleRules(ctx, fakeClient, roleKey, objects)).To(Succeed())
|
||||||
|
|
||||||
|
var after rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, roleKey, &after)).To(Succeed())
|
||||||
|
Expect(after.ResourceVersion).To(Equal(before.ResourceVersion))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should not modify labels", func() {
|
||||||
|
roleKey := client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-cluster-barman-cloud",
|
||||||
|
}
|
||||||
|
|
||||||
|
var before rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, roleKey, &before)).To(Succeed())
|
||||||
|
|
||||||
|
Expect(rbac.EnsureRoleRules(ctx, fakeClient, roleKey, objects)).To(Succeed())
|
||||||
|
|
||||||
|
var after rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, roleKey, &after)).To(Succeed())
|
||||||
|
Expect(after.Labels).To(Equal(before.Labels))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Context("when the Role does not exist", func() {
|
||||||
|
BeforeEach(func() {
|
||||||
|
fakeClient = fake.NewClientBuilder().WithScheme(newScheme()).Build()
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return nil", func() {
|
||||||
|
roleKey := client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "nonexistent-barman-cloud",
|
||||||
|
}
|
||||||
|
err := rbac.EnsureRoleRules(ctx, fakeClient, roleKey, objects)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
32
internal/cnpgi/operator/rbac/suite_test.go
Normal file
32
internal/cnpgi/operator/rbac/suite_test.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package rbac_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRBAC(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "RBAC Suite")
|
||||||
|
}
|
||||||
@ -28,12 +28,12 @@ import (
|
|||||||
"github.com/cloudnative-pg/cnpg-i/pkg/reconciler"
|
"github.com/cloudnative-pg/cnpg-i/pkg/reconciler"
|
||||||
"github.com/cloudnative-pg/machinery/pkg/log"
|
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||||
rbacv1 "k8s.io/api/rbac/v1"
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/equality"
|
|
||||||
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/config"
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/config"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/rbac"
|
||||||
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs"
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -113,7 +113,7 @@ func (r ReconcilerImplementation) Pre(
|
|||||||
barmanObjects = append(barmanObjects, barmanObject)
|
barmanObjects = append(barmanObjects, barmanObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := r.ensureRole(ctx, &cluster, barmanObjects); err != nil {
|
if err := rbac.EnsureRole(ctx, r.Client, &cluster, barmanObjects); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -137,66 +137,15 @@ func (r ReconcilerImplementation) Post(
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r ReconcilerImplementation) ensureRole(
|
|
||||||
ctx context.Context,
|
|
||||||
cluster *cnpgv1.Cluster,
|
|
||||||
barmanObjects []barmancloudv1.ObjectStore,
|
|
||||||
) error {
|
|
||||||
contextLogger := log.FromContext(ctx)
|
|
||||||
newRole := specs.BuildRole(cluster, barmanObjects)
|
|
||||||
|
|
||||||
var role rbacv1.Role
|
|
||||||
if err := r.Client.Get(ctx, client.ObjectKey{
|
|
||||||
Namespace: newRole.Namespace,
|
|
||||||
Name: newRole.Name,
|
|
||||||
}, &role); err != nil {
|
|
||||||
if !apierrs.IsNotFound(err) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
contextLogger.Info(
|
|
||||||
"Creating role",
|
|
||||||
"name", newRole.Name,
|
|
||||||
"namespace", newRole.Namespace,
|
|
||||||
)
|
|
||||||
|
|
||||||
if err := setOwnerReference(cluster, newRole); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return r.Client.Create(ctx, newRole)
|
|
||||||
}
|
|
||||||
|
|
||||||
if equality.Semantic.DeepEqual(newRole.Rules, role.Rules) {
|
|
||||||
// There's no need to hit the API server again
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
contextLogger.Info(
|
|
||||||
"Patching role",
|
|
||||||
"name", newRole.Name,
|
|
||||||
"namespace", newRole.Namespace,
|
|
||||||
"rules", newRole.Rules,
|
|
||||||
)
|
|
||||||
|
|
||||||
oldRole := role.DeepCopy()
|
|
||||||
|
|
||||||
// Apply to the role the new rules
|
|
||||||
role.Rules = newRole.Rules
|
|
||||||
|
|
||||||
// Push it back to the API server
|
|
||||||
return r.Client.Patch(ctx, &role, client.MergeFrom(oldRole))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r ReconcilerImplementation) ensureRoleBinding(
|
func (r ReconcilerImplementation) ensureRoleBinding(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
cluster *cnpgv1.Cluster,
|
cluster *cnpgv1.Cluster,
|
||||||
) error {
|
) error {
|
||||||
var role rbacv1.RoleBinding
|
var roleBinding rbacv1.RoleBinding
|
||||||
if err := r.Client.Get(ctx, client.ObjectKey{
|
if err := r.Client.Get(ctx, client.ObjectKey{
|
||||||
Namespace: cluster.Namespace,
|
Namespace: cluster.Namespace,
|
||||||
Name: specs.GetRBACName(cluster.Name),
|
Name: specs.GetRBACName(cluster.Name),
|
||||||
}, &role); err != nil {
|
}, &roleBinding); err != nil {
|
||||||
if apierrs.IsNotFound(err) {
|
if apierrs.IsNotFound(err) {
|
||||||
return r.createRoleBinding(ctx, cluster)
|
return r.createRoleBinding(ctx, cluster)
|
||||||
}
|
}
|
||||||
@ -213,7 +162,7 @@ func (r ReconcilerImplementation) createRoleBinding(
|
|||||||
cluster *cnpgv1.Cluster,
|
cluster *cnpgv1.Cluster,
|
||||||
) error {
|
) error {
|
||||||
roleBinding := specs.BuildRoleBinding(cluster)
|
roleBinding := specs.BuildRoleBinding(cluster)
|
||||||
if err := setOwnerReference(cluster, roleBinding); err != nil {
|
if err := specs.SetControllerReference(cluster, roleBinding); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return r.Client.Create(ctx, roleBinding)
|
return r.Client.Create(ctx, roleBinding)
|
||||||
|
|||||||
@ -17,7 +17,7 @@ limitations under the License.
|
|||||||
SPDX-License-Identifier: Apache-2.0
|
SPDX-License-Identifier: Apache-2.0
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package operator
|
package specs
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -27,26 +27,32 @@ import (
|
|||||||
"k8s.io/utils/ptr"
|
"k8s.io/utils/ptr"
|
||||||
)
|
)
|
||||||
|
|
||||||
// setOwnerReference explicitly set the owner reference between an
|
// SetControllerReference sets an owner reference on controlled
|
||||||
// owner object and a controller one.
|
// pointing to owner, reading the GVK from the owner object's
|
||||||
|
// metadata rather than from a scheme. This is necessary because
|
||||||
|
// the operator does not know the CNPG API group at compile time
|
||||||
|
// (it may be customized), while the Cluster object decoded from
|
||||||
|
// the gRPC request carries the correct GVK in its TypeMeta.
|
||||||
//
|
//
|
||||||
// Important: this function won't use any registered scheme and will
|
// This function replaces all existing owner references rather than
|
||||||
// fail unless the metadata has been correctly set into the owner
|
// merging, so it assumes the controlled object has a single owner.
|
||||||
// object.
|
// This holds for plugin-managed Roles and RoleBindings, which are
|
||||||
func setOwnerReference(owner, controlled metav1.Object) error {
|
// exclusively owned by one Cluster.
|
||||||
|
func SetControllerReference(owner, controlled metav1.Object) error {
|
||||||
ro, ok := owner.(runtime.Object)
|
ro, ok := owner.(runtime.Object)
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("%T is not a runtime.Object, cannot call setOwnerReference", owner)
|
return fmt.Errorf("%T is not a runtime.Object, cannot call SetControllerReference", owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ro.DeepCopyObject().GetObjectKind().GroupVersionKind().Group) == 0 {
|
gvk := ro.GetObjectKind().GroupVersionKind()
|
||||||
return fmt.Errorf("%T metadata have not been set, cannot call setOwnerReference", owner)
|
if gvk.Kind == "" {
|
||||||
|
return fmt.Errorf("%T has no GVK set in its metadata, cannot call SetControllerReference", owner)
|
||||||
}
|
}
|
||||||
|
|
||||||
controlled.SetOwnerReferences([]metav1.OwnerReference{
|
controlled.SetOwnerReferences([]metav1.OwnerReference{
|
||||||
{
|
{
|
||||||
APIVersion: ro.GetObjectKind().GroupVersionKind().GroupVersion().String(),
|
APIVersion: gvk.GroupVersion().String(),
|
||||||
Kind: ro.GetObjectKind().GroupVersionKind().Kind,
|
Kind: gvk.Kind,
|
||||||
Name: owner.GetName(),
|
Name: owner.GetName(),
|
||||||
UID: owner.GetUID(),
|
UID: owner.GetUID(),
|
||||||
BlockOwnerDeletion: ptr.To(true),
|
BlockOwnerDeletion: ptr.To(true),
|
||||||
100
internal/cnpgi/operator/specs/ownership_test.go
Normal file
100
internal/cnpgi/operator/specs/ownership_test.go
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package specs
|
||||||
|
|
||||||
|
import (
|
||||||
|
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("SetControllerReference", func() {
|
||||||
|
It("should set the owner reference from the owner's TypeMeta", func() {
|
||||||
|
owner := &cnpgv1.Cluster{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
APIVersion: "postgresql.cnpg.io/v1",
|
||||||
|
Kind: "Cluster",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "my-cluster",
|
||||||
|
Namespace: "default",
|
||||||
|
UID: types.UID("test-uid"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
controlled := &rbacv1.Role{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "my-role",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
Expect(SetControllerReference(owner, controlled)).To(Succeed())
|
||||||
|
Expect(controlled.OwnerReferences).To(HaveLen(1))
|
||||||
|
Expect(controlled.OwnerReferences[0].APIVersion).To(Equal("postgresql.cnpg.io/v1"))
|
||||||
|
Expect(controlled.OwnerReferences[0].Kind).To(Equal("Cluster"))
|
||||||
|
Expect(controlled.OwnerReferences[0].Name).To(Equal("my-cluster"))
|
||||||
|
Expect(controlled.OwnerReferences[0].UID).To(Equal(types.UID("test-uid")))
|
||||||
|
Expect(*controlled.OwnerReferences[0].Controller).To(BeTrue())
|
||||||
|
Expect(*controlled.OwnerReferences[0].BlockOwnerDeletion).To(BeTrue())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should work with a custom CNPG API group", func() {
|
||||||
|
owner := &cnpgv1.Cluster{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
APIVersion: "mycompany.io/v1",
|
||||||
|
Kind: "Cluster",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "my-cluster",
|
||||||
|
UID: types.UID("test-uid"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
controlled := &rbacv1.Role{}
|
||||||
|
|
||||||
|
Expect(SetControllerReference(owner, controlled)).To(Succeed())
|
||||||
|
Expect(controlled.OwnerReferences[0].APIVersion).To(Equal("mycompany.io/v1"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should fail when the owner has no GVK set", func() {
|
||||||
|
owner := &cnpgv1.Cluster{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "my-cluster",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
controlled := &rbacv1.Role{}
|
||||||
|
|
||||||
|
err := SetControllerReference(owner, controlled)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(err.Error()).To(ContainSubstring("has no GVK set"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should fail when the owner does not implement runtime.Object", func() {
|
||||||
|
// metav1.ObjectMeta satisfies metav1.Object but not runtime.Object.
|
||||||
|
owner := &metav1.ObjectMeta{Name: "my-cluster"}
|
||||||
|
controlled := &rbacv1.Role{}
|
||||||
|
|
||||||
|
err := SetControllerReference(owner, controlled)
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(err.Error()).To(ContainSubstring("is not a runtime.Object"))
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -21,6 +21,7 @@ package specs
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||||
"github.com/cloudnative-pg/machinery/pkg/stringset"
|
"github.com/cloudnative-pg/machinery/pkg/stringset"
|
||||||
@ -28,6 +29,7 @@ import (
|
|||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BuildRole builds the Role object for this cluster
|
// BuildRole builds the Role object for this cluster
|
||||||
@ -35,15 +37,20 @@ func BuildRole(
|
|||||||
cluster *cnpgv1.Cluster,
|
cluster *cnpgv1.Cluster,
|
||||||
barmanObjects []barmancloudv1.ObjectStore,
|
barmanObjects []barmancloudv1.ObjectStore,
|
||||||
) *rbacv1.Role {
|
) *rbacv1.Role {
|
||||||
role := &rbacv1.Role{
|
return &rbacv1.Role{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Namespace: cluster.Namespace,
|
Namespace: cluster.Namespace,
|
||||||
Name: GetRBACName(cluster.Name),
|
Name: GetRBACName(cluster.Name),
|
||||||
|
Labels: map[string]string{
|
||||||
|
metadata.ClusterLabelName: cluster.Name,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
Rules: BuildRoleRules(barmanObjects),
|
||||||
Rules: []rbacv1.PolicyRule{},
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildRoleRules builds the RBAC PolicyRules for the given ObjectStores.
|
||||||
|
func BuildRoleRules(barmanObjects []barmancloudv1.ObjectStore) []rbacv1.PolicyRule {
|
||||||
secretsSet := stringset.New()
|
secretsSet := stringset.New()
|
||||||
barmanObjectsSet := stringset.New()
|
barmanObjectsSet := stringset.New()
|
||||||
|
|
||||||
@ -54,11 +61,10 @@ func BuildRole(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
role.Rules = append(
|
return []rbacv1.PolicyRule{
|
||||||
role.Rules,
|
{
|
||||||
rbacv1.PolicyRule{
|
|
||||||
APIGroups: []string{
|
APIGroups: []string{
|
||||||
"barmancloud.cnpg.io",
|
barmancloudv1.GroupVersion.Group,
|
||||||
},
|
},
|
||||||
Verbs: []string{
|
Verbs: []string{
|
||||||
"get",
|
"get",
|
||||||
@ -70,9 +76,9 @@ func BuildRole(
|
|||||||
},
|
},
|
||||||
ResourceNames: barmanObjectsSet.ToSortedList(),
|
ResourceNames: barmanObjectsSet.ToSortedList(),
|
||||||
},
|
},
|
||||||
rbacv1.PolicyRule{
|
{
|
||||||
APIGroups: []string{
|
APIGroups: []string{
|
||||||
"barmancloud.cnpg.io",
|
barmancloudv1.GroupVersion.Group,
|
||||||
},
|
},
|
||||||
Verbs: []string{
|
Verbs: []string{
|
||||||
"update",
|
"update",
|
||||||
@ -82,7 +88,7 @@ func BuildRole(
|
|||||||
},
|
},
|
||||||
ResourceNames: barmanObjectsSet.ToSortedList(),
|
ResourceNames: barmanObjectsSet.ToSortedList(),
|
||||||
},
|
},
|
||||||
rbacv1.PolicyRule{
|
{
|
||||||
APIGroups: []string{
|
APIGroups: []string{
|
||||||
"",
|
"",
|
||||||
},
|
},
|
||||||
@ -96,9 +102,25 @@ func BuildRole(
|
|||||||
},
|
},
|
||||||
ResourceNames: secretsSet.ToSortedList(),
|
ResourceNames: secretsSet.ToSortedList(),
|
||||||
},
|
},
|
||||||
)
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return role
|
// ObjectStoreNamesFromRole extracts the ObjectStore names referenced
|
||||||
|
// by a plugin-managed Role. It finds the objectstores rule
|
||||||
|
// semantically (by APIGroup and Resource, not by index) and returns
|
||||||
|
// a copy of its ResourceNames. Returns nil if no matching rule is
|
||||||
|
// found.
|
||||||
|
func ObjectStoreNamesFromRole(role *rbacv1.Role) []string {
|
||||||
|
for _, rule := range role.Rules {
|
||||||
|
if len(rule.APIGroups) == 1 &&
|
||||||
|
rule.APIGroups[0] == barmancloudv1.GroupVersion.Group &&
|
||||||
|
len(rule.Resources) == 1 &&
|
||||||
|
rule.Resources[0] == "objectstores" {
|
||||||
|
return slices.Clone(rule.ResourceNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// BuildRoleBinding builds the role binding object for this cluster
|
// BuildRoleBinding builds the role binding object for this cluster
|
||||||
|
|||||||
210
internal/cnpgi/operator/specs/role_test.go
Normal file
210
internal/cnpgi/operator/specs/role_test.go
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package specs
|
||||||
|
|
||||||
|
import (
|
||||||
|
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
|
||||||
|
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||||
|
machineryapi "github.com/cloudnative-pg/machinery/pkg/api"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
|
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
func newTestObjectStore(name, secretName string) barmancloudv1.ObjectStore {
|
||||||
|
return barmancloudv1.ObjectStore{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
Spec: barmancloudv1.ObjectStoreSpec{
|
||||||
|
Configuration: barmanapi.BarmanObjectStoreConfiguration{
|
||||||
|
DestinationPath: "s3://bucket/path",
|
||||||
|
BarmanCredentials: barmanapi.BarmanCredentials{
|
||||||
|
AWS: &barmanapi.S3Credentials{
|
||||||
|
AccessKeyIDReference: &machineryapi.SecretKeySelector{
|
||||||
|
LocalObjectReference: machineryapi.LocalObjectReference{
|
||||||
|
Name: secretName,
|
||||||
|
},
|
||||||
|
Key: "ACCESS_KEY_ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ = Describe("BuildRoleRules", func() {
|
||||||
|
It("should produce 3 rules with correct ResourceNames", func() {
|
||||||
|
objects := []barmancloudv1.ObjectStore{
|
||||||
|
newTestObjectStore("store-a", "secret-a"),
|
||||||
|
newTestObjectStore("store-b", "secret-b"),
|
||||||
|
}
|
||||||
|
rules := BuildRoleRules(objects)
|
||||||
|
Expect(rules).To(HaveLen(3))
|
||||||
|
|
||||||
|
Expect(rules[0].APIGroups).To(Equal([]string{barmancloudv1.GroupVersion.Group}))
|
||||||
|
Expect(rules[0].Resources).To(Equal([]string{"objectstores"}))
|
||||||
|
Expect(rules[0].ResourceNames).To(ConsistOf("store-a", "store-b"))
|
||||||
|
|
||||||
|
Expect(rules[1].APIGroups).To(Equal([]string{barmancloudv1.GroupVersion.Group}))
|
||||||
|
Expect(rules[1].Resources).To(Equal([]string{"objectstores/status"}))
|
||||||
|
Expect(rules[1].ResourceNames).To(ConsistOf("store-a", "store-b"))
|
||||||
|
|
||||||
|
Expect(rules[2].APIGroups).To(Equal([]string{""}))
|
||||||
|
Expect(rules[2].Resources).To(Equal([]string{"secrets"}))
|
||||||
|
Expect(rules[2].ResourceNames).To(ConsistOf("secret-a", "secret-b"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should produce rules with empty ResourceNames for empty input", func() {
|
||||||
|
rules := BuildRoleRules(nil)
|
||||||
|
Expect(rules).To(HaveLen(3))
|
||||||
|
Expect(rules[0].ResourceNames).To(BeEmpty())
|
||||||
|
Expect(rules[0].ResourceNames).NotTo(BeNil())
|
||||||
|
Expect(rules[1].ResourceNames).To(BeEmpty())
|
||||||
|
Expect(rules[2].ResourceNames).To(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should deduplicate secret names across ObjectStores", func() {
|
||||||
|
objects := []barmancloudv1.ObjectStore{
|
||||||
|
newTestObjectStore("store-a", "shared-secret"),
|
||||||
|
newTestObjectStore("store-b", "shared-secret"),
|
||||||
|
}
|
||||||
|
rules := BuildRoleRules(objects)
|
||||||
|
Expect(rules[2].ResourceNames).To(Equal([]string{"shared-secret"}))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
var _ = Describe("BuildRole", func() {
|
||||||
|
It("should set the cluster label", func() {
|
||||||
|
cluster := &cnpgv1.Cluster{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "my-cluster",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
role := BuildRole(cluster, nil)
|
||||||
|
Expect(role.Labels).To(HaveKeyWithValue(metadata.ClusterLabelName, "my-cluster"))
|
||||||
|
Expect(role.Name).To(Equal("my-cluster-barman-cloud"))
|
||||||
|
Expect(role.Namespace).To(Equal("default"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
var _ = Describe("BuildRoleRules / ObjectStoreNamesFromRole round-trip", func() {
|
||||||
|
It("should recover the same ObjectStore names from built rules", func() {
|
||||||
|
objects := []barmancloudv1.ObjectStore{
|
||||||
|
newTestObjectStore("store-a", "secret-a"),
|
||||||
|
newTestObjectStore("store-b", "secret-b"),
|
||||||
|
}
|
||||||
|
rules := BuildRoleRules(objects)
|
||||||
|
role := &rbacv1.Role{Rules: rules}
|
||||||
|
names := ObjectStoreNamesFromRole(role)
|
||||||
|
Expect(names).To(ConsistOf("store-a", "store-b"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should recover empty names from rules built with no ObjectStores", func() {
|
||||||
|
rules := BuildRoleRules(nil)
|
||||||
|
role := &rbacv1.Role{Rules: rules}
|
||||||
|
names := ObjectStoreNamesFromRole(role)
|
||||||
|
Expect(names).To(BeEmpty())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
var _ = Describe("ObjectStoreNamesFromRole", func() {
|
||||||
|
It("should extract ObjectStore names from a well-formed Role", func() {
|
||||||
|
role := &rbacv1.Role{
|
||||||
|
Rules: []rbacv1.PolicyRule{
|
||||||
|
{
|
||||||
|
APIGroups: []string{barmancloudv1.GroupVersion.Group},
|
||||||
|
Resources: []string{"objectstores"},
|
||||||
|
ResourceNames: []string{"store-a", "store-b"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
APIGroups: []string{""},
|
||||||
|
Resources: []string{"secrets"},
|
||||||
|
ResourceNames: []string{"secret-a"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(ObjectStoreNamesFromRole(role)).To(Equal([]string{"store-a", "store-b"}))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return nil for a Role with no matching rule", func() {
|
||||||
|
role := &rbacv1.Role{
|
||||||
|
Rules: []rbacv1.PolicyRule{
|
||||||
|
{
|
||||||
|
APIGroups: []string{""},
|
||||||
|
Resources: []string{"secrets"},
|
||||||
|
ResourceNames: []string{"secret-a"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(ObjectStoreNamesFromRole(role)).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return nil for a Role with empty rules", func() {
|
||||||
|
role := &rbacv1.Role{}
|
||||||
|
Expect(ObjectStoreNamesFromRole(role)).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should not match a rule with a different APIGroup", func() {
|
||||||
|
role := &rbacv1.Role{
|
||||||
|
Rules: []rbacv1.PolicyRule{
|
||||||
|
{
|
||||||
|
APIGroups: []string{"other.io"},
|
||||||
|
Resources: []string{"objectstores"},
|
||||||
|
ResourceNames: []string{"store-a"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(ObjectStoreNamesFromRole(role)).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should not match a rule with multiple APIGroups", func() {
|
||||||
|
role := &rbacv1.Role{
|
||||||
|
Rules: []rbacv1.PolicyRule{
|
||||||
|
{
|
||||||
|
APIGroups: []string{barmancloudv1.GroupVersion.Group, "other.io"},
|
||||||
|
Resources: []string{"objectstores"},
|
||||||
|
ResourceNames: []string{"store-a"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(ObjectStoreNamesFromRole(role)).To(BeNil())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should not match a rule for objectstores/status", func() {
|
||||||
|
role := &rbacv1.Role{
|
||||||
|
Rules: []rbacv1.PolicyRule{
|
||||||
|
{
|
||||||
|
APIGroups: []string{barmancloudv1.GroupVersion.Group},
|
||||||
|
Resources: []string{"objectstores/status"},
|
||||||
|
ResourceNames: []string{"store-a"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(ObjectStoreNamesFromRole(role)).To(BeNil())
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -22,34 +22,23 @@ package restore
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
|
||||||
"github.com/cloudnative-pg/machinery/pkg/log"
|
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
|
||||||
clientgoscheme "k8s.io/client-go/kubernetes/scheme"
|
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
var scheme = runtime.NewScheme()
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
utilruntime.Must(barmancloudv1.AddToScheme(scheme))
|
|
||||||
utilruntime.Must(cnpgv1.AddToScheme(scheme))
|
|
||||||
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start starts the sidecar informers and CNPG-i server
|
// Start starts the sidecar informers and CNPG-i server
|
||||||
func Start(ctx context.Context) error {
|
func Start(ctx context.Context) error {
|
||||||
setupLog := log.FromContext(ctx)
|
setupLog := log.FromContext(ctx)
|
||||||
setupLog.Info("Starting barman cloud instance plugin")
|
setupLog.Info("Starting barman cloud instance plugin")
|
||||||
|
|
||||||
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{
|
||||||
Scheme: scheme,
|
Scheme: common.GenerateScheme(ctx),
|
||||||
Client: client.Options{
|
Client: client.Options{
|
||||||
Cache: &client.CacheOptions{
|
Cache: &client.CacheOptions{
|
||||||
DisableFor: []client.Object{
|
DisableFor: []client.Object{
|
||||||
|
|||||||
@ -21,14 +21,23 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/cloudnative-pg/machinery/pkg/log"
|
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||||
|
|
||||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/rbac"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ObjectStoreReconciler reconciles a ObjectStore object.
|
// ObjectStoreReconciler reconciles a ObjectStore object.
|
||||||
@ -40,33 +49,93 @@ type ObjectStoreReconciler struct {
|
|||||||
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;patch;update;get;list;watch
|
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=rolebindings,verbs=create;patch;update;get;list;watch
|
||||||
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;patch;update;get;list;watch
|
// +kubebuilder:rbac:groups=rbac.authorization.k8s.io,resources=roles,verbs=create;patch;update;get;list;watch
|
||||||
// +kubebuilder:rbac:groups="",resources=secrets,verbs=create;list;get;watch;delete
|
// +kubebuilder:rbac:groups="",resources=secrets,verbs=create;list;get;watch;delete
|
||||||
// +kubebuilder:rbac:groups=postgresql.cnpg.io,resources=clusters/finalizers,verbs=update
|
|
||||||
// +kubebuilder:rbac:groups=postgresql.cnpg.io,resources=backups,verbs=get;list;watch
|
// +kubebuilder:rbac:groups=postgresql.cnpg.io,resources=backups,verbs=get;list;watch
|
||||||
|
// +kubebuilder:rbac:groups=postgresql.cnpg.io,resources=clusters/finalizers,verbs=update
|
||||||
// +kubebuilder:rbac:groups=barmancloud.cnpg.io,resources=objectstores,verbs=get;list;watch;create;update;patch;delete
|
// +kubebuilder:rbac:groups=barmancloud.cnpg.io,resources=objectstores,verbs=get;list;watch;create;update;patch;delete
|
||||||
// +kubebuilder:rbac:groups=barmancloud.cnpg.io,resources=objectstores/status,verbs=get;update;patch
|
// +kubebuilder:rbac:groups=barmancloud.cnpg.io,resources=objectstores/status,verbs=get;update;patch
|
||||||
// +kubebuilder:rbac:groups=barmancloud.cnpg.io,resources=objectstores/finalizers,verbs=update
|
// +kubebuilder:rbac:groups=barmancloud.cnpg.io,resources=objectstores/finalizers,verbs=update
|
||||||
|
|
||||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
// Reconcile ensures that the RBAC Role for each Cluster referencing
|
||||||
// move the current state of the cluster closer to the desired state.
|
// this ObjectStore is up to date with the current ObjectStore spec.
|
||||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
// It discovers affected Roles by listing plugin-managed Roles and
|
||||||
// the ObjectStore object against the actual cluster state, and then
|
// inspecting their rules, without needing access to Cluster objects.
|
||||||
// perform operations to make the cluster state reflect the state specified by
|
func (r *ObjectStoreReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||||
// the user.
|
contextLogger := log.FromContext(ctx).WithValues(
|
||||||
//
|
"objectStoreName", req.Name,
|
||||||
// For more details, check Reconcile and its Result here:
|
"namespace", req.Namespace,
|
||||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.0/pkg/reconcile
|
)
|
||||||
func (r *ObjectStoreReconciler) Reconcile(ctx context.Context, _ ctrl.Request) (ctrl.Result, error) {
|
ctx = log.IntoContext(ctx, contextLogger)
|
||||||
_ = log.FromContext(ctx)
|
|
||||||
|
|
||||||
// TODO(user): your logic here
|
contextLogger.Info("ObjectStore reconciliation start")
|
||||||
|
|
||||||
return ctrl.Result{}, nil
|
// NOTE: Roles created before the introduction of ClusterLabelName
|
||||||
|
// are not discovered here. The Pre hook patches the label on every
|
||||||
|
// Cluster reconciliation, so unlabeled Roles are picked up after
|
||||||
|
// the next Cluster reconcile cycle.
|
||||||
|
var roleList rbacv1.RoleList
|
||||||
|
if err := r.List(ctx, &roleList,
|
||||||
|
client.InNamespace(req.Namespace),
|
||||||
|
client.HasLabels{metadata.ClusterLabelName},
|
||||||
|
); err != nil {
|
||||||
|
return ctrl.Result{}, fmt.Errorf("while listing roles: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs []error
|
||||||
|
for i := range roleList.Items {
|
||||||
|
role := &roleList.Items[i]
|
||||||
|
|
||||||
|
objectStoreNames := specs.ObjectStoreNamesFromRole(role)
|
||||||
|
if !slices.Contains(objectStoreNames, req.Name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
contextLogger.Info("Reconciling RBAC for role",
|
||||||
|
"roleName", role.Name)
|
||||||
|
|
||||||
|
if err := r.reconcileRoleRules(ctx, role, objectStoreNames); err != nil {
|
||||||
|
contextLogger.Error(err, "Failed to reconcile RBAC for role",
|
||||||
|
"roleName", role.Name)
|
||||||
|
errs = append(errs, fmt.Errorf("while reconciling role %s: %w", role.Name, err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
contextLogger.Info("ObjectStore reconciliation completed")
|
||||||
|
return ctrl.Result{}, errors.Join(errs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// reconcileRoleRules fetches the ObjectStores referenced by the
|
||||||
|
// Role and patches its rules to match the current specs.
|
||||||
|
func (r *ObjectStoreReconciler) reconcileRoleRules(
|
||||||
|
ctx context.Context,
|
||||||
|
role *rbacv1.Role,
|
||||||
|
objectStoreNames []string,
|
||||||
|
) error {
|
||||||
|
contextLogger := log.FromContext(ctx)
|
||||||
|
barmanObjects := make([]barmancloudv1.ObjectStore, 0, len(objectStoreNames))
|
||||||
|
|
||||||
|
for _, name := range objectStoreNames {
|
||||||
|
var barmanObject barmancloudv1.ObjectStore
|
||||||
|
if err := r.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: role.Namespace,
|
||||||
|
Name: name,
|
||||||
|
}, &barmanObject); err != nil {
|
||||||
|
if apierrs.IsNotFound(err) {
|
||||||
|
contextLogger.Info("ObjectStore not found, skipping",
|
||||||
|
"objectStoreName", name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fmt.Errorf("while getting ObjectStore %s: %w", name, err)
|
||||||
|
}
|
||||||
|
barmanObjects = append(barmanObjects, barmanObject)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rbac.EnsureRoleRules(ctx, r.Client, client.ObjectKeyFromObject(role), barmanObjects)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupWithManager sets up the controller with the Manager.
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
func (r *ObjectStoreReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
func (r *ObjectStoreReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
err := ctrl.NewControllerManagedBy(mgr).
|
err := ctrl.NewControllerManagedBy(mgr).
|
||||||
For(&barmancloudv1.ObjectStore{}).
|
For(&barmancloudv1.ObjectStore{}, builder.WithPredicates(predicate.GenerationChangedPredicate{})).
|
||||||
Complete(r)
|
Complete(r)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to create controller: %w", err)
|
return fmt.Errorf("unable to create controller: %w", err)
|
||||||
|
|||||||
@ -21,71 +21,382 @@ package controller
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
|
barmanapi "github.com/cloudnative-pg/barman-cloud/pkg/api"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
machineryapi "github.com/cloudnative-pg/machinery/pkg/api"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
apierrs "k8s.io/apimachinery/pkg/api/errors"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||||
|
|
||||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
|
||||||
. "github.com/onsi/ginkgo/v2"
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs"
|
||||||
. "github.com/onsi/gomega"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ = Describe("ObjectStore Controller", func() {
|
func newFakeScheme() *runtime.Scheme {
|
||||||
Context("When reconciling a resource", func() {
|
s := runtime.NewScheme()
|
||||||
const resourceName = "test-resource"
|
utilruntime.Must(rbacv1.AddToScheme(s))
|
||||||
|
utilruntime.Must(barmancloudv1.AddToScheme(s))
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
func newTestObjectStore(name, namespace, secretName string) *barmancloudv1.ObjectStore {
|
||||||
|
return &barmancloudv1.ObjectStore{
|
||||||
typeNamespacedName := types.NamespacedName{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: resourceName,
|
Name: name,
|
||||||
Namespace: "default", // TODO(user):Modify as needed
|
Namespace: namespace,
|
||||||
}
|
},
|
||||||
objectstore := &barmancloudv1.ObjectStore{}
|
Spec: barmancloudv1.ObjectStoreSpec{
|
||||||
|
Configuration: barmanapi.BarmanObjectStoreConfiguration{
|
||||||
BeforeEach(func() {
|
DestinationPath: "s3://bucket/path",
|
||||||
By("creating the custom resource for the Kind ObjectStore")
|
BarmanCredentials: barmanapi.BarmanCredentials{
|
||||||
err := k8sClient.Get(ctx, typeNamespacedName, objectstore)
|
AWS: &barmanapi.S3Credentials{
|
||||||
if err != nil && errors.IsNotFound(err) {
|
AccessKeyIDReference: &machineryapi.SecretKeySelector{
|
||||||
resource := &barmancloudv1.ObjectStore{
|
LocalObjectReference: machineryapi.LocalObjectReference{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
Name: secretName,
|
||||||
Name: resourceName,
|
},
|
||||||
Namespace: "default",
|
Key: "ACCESS_KEY_ID",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Spec: barmancloudv1.ObjectStoreSpec{
|
},
|
||||||
Configuration: barmanapi.BarmanObjectStoreConfiguration{DestinationPath: "/tmp"},
|
},
|
||||||
},
|
},
|
||||||
// TODO(user): Specify other spec details if needed.
|
}
|
||||||
}
|
}
|
||||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
AfterEach(func() {
|
func newLabeledRole(clusterName, namespace string, objectStores []barmancloudv1.ObjectStore) *rbacv1.Role {
|
||||||
// TODO(user): Cleanup logic after each test, like removing the resource instance.
|
return &rbacv1.Role{
|
||||||
resource := &barmancloudv1.ObjectStore{}
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
Name: specs.GetRBACName(clusterName),
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Namespace: namespace,
|
||||||
|
Labels: map[string]string{
|
||||||
|
metadata.ClusterLabelName: clusterName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Rules: specs.BuildRoleRules(objectStores),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
By("Cleanup the specific resource instance ObjectStore")
|
var _ = Describe("ObjectStoreReconciler", func() {
|
||||||
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
var (
|
||||||
})
|
ctx context.Context
|
||||||
It("should successfully reconcile the resource", func() {
|
scheme *runtime.Scheme
|
||||||
By("Reconciling the created resource")
|
)
|
||||||
controllerReconciler := &ObjectStoreReconciler{
|
|
||||||
Client: k8sClient,
|
BeforeEach(func() {
|
||||||
Scheme: k8sClient.Scheme(),
|
ctx = context.Background()
|
||||||
|
scheme = newFakeScheme()
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("Reconcile", func() {
|
||||||
|
It("should update Role rules when ObjectStore credentials change", func() {
|
||||||
|
oldStore := newTestObjectStore("my-store", "default", "old-secret")
|
||||||
|
role := newLabeledRole("my-cluster", "default", []barmancloudv1.ObjectStore{*oldStore})
|
||||||
|
|
||||||
|
// Update the ObjectStore with new credentials
|
||||||
|
newStore := newTestObjectStore("my-store", "default", "new-secret")
|
||||||
|
|
||||||
|
fakeClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
WithObjects(role, newStore).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
reconciler := &ObjectStoreReconciler{
|
||||||
|
Client: fakeClient,
|
||||||
|
Scheme: scheme,
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
result, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||||
NamespacedName: typeNamespacedName,
|
NamespacedName: types.NamespacedName{
|
||||||
|
Name: "my-store",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
})
|
})
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
|
Expect(result).To(Equal(reconcile.Result{}))
|
||||||
// Example: If you expect a certain status condition after reconciliation, verify it here.
|
|
||||||
|
var updatedRole rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "my-cluster-barman-cloud",
|
||||||
|
}, &updatedRole)).To(Succeed())
|
||||||
|
|
||||||
|
secretsRule := updatedRole.Rules[2]
|
||||||
|
Expect(secretsRule.ResourceNames).To(ContainElement("new-secret"))
|
||||||
|
Expect(secretsRule.ResourceNames).NotTo(ContainElement("old-secret"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should skip Roles that don't reference the ObjectStore", func() {
|
||||||
|
otherStore := newTestObjectStore("other-store", "default", "other-creds")
|
||||||
|
role := newLabeledRole("my-cluster", "default", []barmancloudv1.ObjectStore{*otherStore})
|
||||||
|
|
||||||
|
fakeClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
WithObjects(role).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
reconciler := &ObjectStoreReconciler{
|
||||||
|
Client: fakeClient,
|
||||||
|
Scheme: scheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
var before rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "my-cluster-barman-cloud",
|
||||||
|
}, &before)).To(Succeed())
|
||||||
|
|
||||||
|
result, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||||
|
NamespacedName: types.NamespacedName{
|
||||||
|
Name: "unrelated-store",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(result).To(Equal(reconcile.Result{}))
|
||||||
|
|
||||||
|
var after rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "my-cluster-barman-cloud",
|
||||||
|
}, &after)).To(Succeed())
|
||||||
|
|
||||||
|
Expect(after.ResourceVersion).To(Equal(before.ResourceVersion))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should succeed with no labeled Roles in the namespace", func() {
|
||||||
|
fakeClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
reconciler := &ObjectStoreReconciler{
|
||||||
|
Client: fakeClient,
|
||||||
|
Scheme: scheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||||
|
NamespacedName: types.NamespacedName{
|
||||||
|
Name: "my-store",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(result).To(Equal(reconcile.Result{}))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should handle deleted ObjectStores gracefully", func() {
|
||||||
|
storeA := newTestObjectStore("store-a", "default", "secret-a")
|
||||||
|
storeB := newTestObjectStore("store-b", "default", "secret-b")
|
||||||
|
role := newLabeledRole("my-cluster", "default", []barmancloudv1.ObjectStore{*storeA, *storeB})
|
||||||
|
|
||||||
|
// Only store-a exists; store-b was deleted
|
||||||
|
fakeClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
WithObjects(role, storeA).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
reconciler := &ObjectStoreReconciler{
|
||||||
|
Client: fakeClient,
|
||||||
|
Scheme: scheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||||
|
NamespacedName: types.NamespacedName{
|
||||||
|
Name: "store-b",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(result).To(Equal(reconcile.Result{}))
|
||||||
|
|
||||||
|
var updatedRole rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "my-cluster-barman-cloud",
|
||||||
|
}, &updatedRole)).To(Succeed())
|
||||||
|
|
||||||
|
objectStoreRule := updatedRole.Rules[0]
|
||||||
|
Expect(objectStoreRule.ResourceNames).To(ContainElement("store-a"))
|
||||||
|
Expect(objectStoreRule.ResourceNames).NotTo(ContainElement("store-b"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should not panic on a Role with empty rules", func() {
|
||||||
|
emptyRole := &rbacv1.Role{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "empty-barman-cloud",
|
||||||
|
Namespace: "default",
|
||||||
|
Labels: map[string]string{
|
||||||
|
metadata.ClusterLabelName: "empty",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
fakeClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
WithObjects(emptyRole).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
reconciler := &ObjectStoreReconciler{
|
||||||
|
Client: fakeClient,
|
||||||
|
Scheme: scheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||||
|
NamespacedName: types.NamespacedName{
|
||||||
|
Name: "my-store",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(result).To(Equal(reconcile.Result{}))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should produce empty ResourceNames when all ObjectStores are deleted", func() {
|
||||||
|
store := newTestObjectStore("my-store", "default", "aws-creds")
|
||||||
|
role := newLabeledRole("my-cluster", "default", []barmancloudv1.ObjectStore{*store})
|
||||||
|
|
||||||
|
// Don't add the ObjectStore to the fake client (simulates deletion)
|
||||||
|
fakeClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
WithObjects(role).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
reconciler := &ObjectStoreReconciler{
|
||||||
|
Client: fakeClient,
|
||||||
|
Scheme: scheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||||
|
NamespacedName: types.NamespacedName{
|
||||||
|
Name: "my-store",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(result).To(Equal(reconcile.Result{}))
|
||||||
|
|
||||||
|
var updatedRole rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "my-cluster-barman-cloud",
|
||||||
|
}, &updatedRole)).To(Succeed())
|
||||||
|
|
||||||
|
// All rules should have empty ResourceNames
|
||||||
|
Expect(updatedRole.Rules[0].ResourceNames).To(BeEmpty())
|
||||||
|
Expect(updatedRole.Rules[1].ResourceNames).To(BeEmpty())
|
||||||
|
Expect(updatedRole.Rules[2].ResourceNames).To(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return an error when listing Roles fails", func() {
|
||||||
|
internalErr := apierrs.NewInternalError(fmt.Errorf("etcd timeout"))
|
||||||
|
fakeClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
WithInterceptorFuncs(interceptor.Funcs{
|
||||||
|
List: func(ctx context.Context, c client.WithWatch, list client.ObjectList, opts ...client.ListOption) error {
|
||||||
|
if _, ok := list.(*rbacv1.RoleList); ok {
|
||||||
|
return internalErr
|
||||||
|
}
|
||||||
|
return c.List(ctx, list, opts...)
|
||||||
|
},
|
||||||
|
}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
reconciler := &ObjectStoreReconciler{Client: fakeClient, Scheme: scheme}
|
||||||
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||||
|
NamespacedName: types.NamespacedName{Name: "my-store", Namespace: "default"},
|
||||||
|
})
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(err.Error()).To(ContainSubstring("while listing roles"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should return an error when fetching an ObjectStore fails with a transient error", func() {
|
||||||
|
store := newTestObjectStore("my-store", "default", "aws-creds")
|
||||||
|
role := newLabeledRole("my-cluster", "default", []barmancloudv1.ObjectStore{*store})
|
||||||
|
|
||||||
|
internalErr := apierrs.NewInternalError(fmt.Errorf("etcd timeout"))
|
||||||
|
fakeClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
WithObjects(role).
|
||||||
|
WithInterceptorFuncs(interceptor.Funcs{
|
||||||
|
Get: func(ctx context.Context, c client.WithWatch, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
|
||||||
|
if _, ok := obj.(*barmancloudv1.ObjectStore); ok {
|
||||||
|
return internalErr
|
||||||
|
}
|
||||||
|
return c.Get(ctx, key, obj, opts...)
|
||||||
|
},
|
||||||
|
}).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
reconciler := &ObjectStoreReconciler{Client: fakeClient, Scheme: scheme}
|
||||||
|
_, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||||
|
NamespacedName: types.NamespacedName{Name: "my-store", Namespace: "default"},
|
||||||
|
})
|
||||||
|
Expect(err).To(HaveOccurred())
|
||||||
|
Expect(err.Error()).To(ContainSubstring("while reconciling role"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should reconcile multiple Roles referencing the same ObjectStore", func() {
|
||||||
|
store := newTestObjectStore("shared-store", "default", "new-secret")
|
||||||
|
oldStore := barmancloudv1.ObjectStore{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "shared-store", Namespace: "default"},
|
||||||
|
Spec: barmancloudv1.ObjectStoreSpec{
|
||||||
|
Configuration: barmanapi.BarmanObjectStoreConfiguration{
|
||||||
|
DestinationPath: "s3://bucket/path",
|
||||||
|
BarmanCredentials: barmanapi.BarmanCredentials{
|
||||||
|
AWS: &barmanapi.S3Credentials{
|
||||||
|
AccessKeyIDReference: &machineryapi.SecretKeySelector{
|
||||||
|
LocalObjectReference: machineryapi.LocalObjectReference{Name: "old-secret"},
|
||||||
|
Key: "ACCESS_KEY_ID",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
role1 := newLabeledRole("cluster-1", "default", []barmancloudv1.ObjectStore{oldStore})
|
||||||
|
role2 := newLabeledRole("cluster-2", "default", []barmancloudv1.ObjectStore{oldStore})
|
||||||
|
|
||||||
|
fakeClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
WithObjects(role1, role2, store).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
reconciler := &ObjectStoreReconciler{
|
||||||
|
Client: fakeClient,
|
||||||
|
Scheme: scheme,
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := reconciler.Reconcile(ctx, reconcile.Request{
|
||||||
|
NamespacedName: types.NamespacedName{
|
||||||
|
Name: "shared-store",
|
||||||
|
Namespace: "default",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(result).To(Equal(reconcile.Result{}))
|
||||||
|
|
||||||
|
for _, clusterName := range []string{"cluster-1", "cluster-2"} {
|
||||||
|
var updatedRole rbacv1.Role
|
||||||
|
Expect(fakeClient.Get(ctx, client.ObjectKey{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: specs.GetRBACName(clusterName),
|
||||||
|
}, &updatedRole)).To(Succeed())
|
||||||
|
|
||||||
|
secretsRule := updatedRole.Rules[2]
|
||||||
|
Expect(secretsRule.ResourceNames).To(ContainElement("new-secret"))
|
||||||
|
Expect(secretsRule.ResourceNames).NotTo(ContainElement("old-secret"))
|
||||||
|
}
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -20,81 +20,14 @@ SPDX-License-Identifier: Apache-2.0
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
// +kubebuilder:scaffold:imports
|
|
||||||
"k8s.io/client-go/kubernetes/scheme"
|
|
||||||
"k8s.io/client-go/rest"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
|
||||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
|
||||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
|
||||||
|
|
||||||
barmancloudv1 "github.com/cloudnative-pg/plugin-barman-cloud/api/v1"
|
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
. "github.com/onsi/gomega"
|
. "github.com/onsi/gomega"
|
||||||
)
|
)
|
||||||
|
|
||||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
|
||||||
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
|
||||||
|
|
||||||
var (
|
|
||||||
cfg *rest.Config
|
|
||||||
k8sClient client.Client
|
|
||||||
testEnv *envtest.Environment
|
|
||||||
ctx context.Context
|
|
||||||
cancel context.CancelFunc
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestControllers(t *testing.T) {
|
func TestControllers(t *testing.T) {
|
||||||
RegisterFailHandler(Fail)
|
RegisterFailHandler(Fail)
|
||||||
|
|
||||||
RunSpecs(t, "Controller Suite")
|
RunSpecs(t, "Controller Suite")
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ = BeforeSuite(func() {
|
|
||||||
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
|
|
||||||
|
|
||||||
ctx, cancel = context.WithCancel(context.TODO())
|
|
||||||
|
|
||||||
By("bootstrapping test environment")
|
|
||||||
testEnv = &envtest.Environment{
|
|
||||||
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
|
|
||||||
ErrorIfCRDPathMissing: true,
|
|
||||||
|
|
||||||
// The BinaryAssetsDirectory is only required if you want to run the tests directly
|
|
||||||
// without call the makefile target test. If not informed it will look for the
|
|
||||||
// default path defined in controller-runtime which is /usr/local/kubebuilder/.
|
|
||||||
// Note that you must have the required binaries setup under the bin directory to perform
|
|
||||||
// the tests directly. When we run make test it will be setup and used automatically.
|
|
||||||
BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s",
|
|
||||||
fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)),
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
// cfg is defined in this file globally.
|
|
||||||
cfg, err = testEnv.Start()
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
Expect(cfg).NotTo(BeNil())
|
|
||||||
|
|
||||||
err = barmancloudv1.AddToScheme(scheme.Scheme)
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
|
|
||||||
// +kubebuilder:scaffold:scheme
|
|
||||||
|
|
||||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
Expect(k8sClient).NotTo(BeNil())
|
|
||||||
})
|
|
||||||
|
|
||||||
var _ = AfterSuite(func() {
|
|
||||||
By("tearing down the test environment")
|
|
||||||
cancel()
|
|
||||||
err := testEnv.Stop()
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
})
|
|
||||||
|
|||||||
56
internal/scheme/cnpg.go
Normal file
56
internal/scheme/cnpg.go
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||||
|
"github.com/cloudnative-pg/machinery/pkg/log"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
|
crscheme "sigs.k8s.io/controller-runtime/pkg/scheme"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AddCNPGToScheme registers CNPG types into the given scheme using
|
||||||
|
// the API group configured via CUSTOM_CNPG_GROUP/CUSTOM_CNPG_VERSION
|
||||||
|
// environment variables, defaulting to postgresql.cnpg.io/v1.
|
||||||
|
// This allows the plugin to work with any CNPG-based operator.
|
||||||
|
func AddCNPGToScheme(ctx context.Context, s *runtime.Scheme) {
|
||||||
|
cnpgGroup := viper.GetString("custom-cnpg-group")
|
||||||
|
cnpgVersion := viper.GetString("custom-cnpg-version")
|
||||||
|
if len(cnpgGroup) == 0 {
|
||||||
|
cnpgGroup = cnpgv1.SchemeGroupVersion.Group
|
||||||
|
}
|
||||||
|
if len(cnpgVersion) == 0 {
|
||||||
|
cnpgVersion = cnpgv1.SchemeGroupVersion.Version
|
||||||
|
}
|
||||||
|
|
||||||
|
schemeGroupVersion := schema.GroupVersion{Group: cnpgGroup, Version: cnpgVersion}
|
||||||
|
schemeBuilder := &crscheme.Builder{GroupVersion: schemeGroupVersion}
|
||||||
|
schemeBuilder.Register(&cnpgv1.Cluster{}, &cnpgv1.ClusterList{})
|
||||||
|
schemeBuilder.Register(&cnpgv1.Backup{}, &cnpgv1.BackupList{})
|
||||||
|
schemeBuilder.Register(&cnpgv1.ScheduledBackup{}, &cnpgv1.ScheduledBackupList{})
|
||||||
|
utilruntime.Must(schemeBuilder.AddToScheme(s))
|
||||||
|
|
||||||
|
log.FromContext(ctx).Info("CNPG types registration", "schemeGroupVersion", schemeGroupVersion)
|
||||||
|
}
|
||||||
116
internal/scheme/cnpg_test.go
Normal file
116
internal/scheme/cnpg_test.go
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
cnpgv1 "github.com/cloudnative-pg/cloudnative-pg/api/v1"
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("AddCNPGToScheme", func() {
|
||||||
|
var s *runtime.Scheme
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
s = runtime.NewScheme()
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
viper.Reset()
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should register CNPG types under the default group and version", func() {
|
||||||
|
AddCNPGToScheme(context.Background(), s)
|
||||||
|
|
||||||
|
gvks, _, err := s.ObjectKinds(&cnpgv1.Cluster{})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(gvks).To(ContainElement(schema.GroupVersionKind{
|
||||||
|
Group: cnpgv1.SchemeGroupVersion.Group,
|
||||||
|
Version: cnpgv1.SchemeGroupVersion.Version,
|
||||||
|
Kind: "Cluster",
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should register Backup and ScheduledBackup under the default group", func() {
|
||||||
|
AddCNPGToScheme(context.Background(), s)
|
||||||
|
|
||||||
|
gvks, _, err := s.ObjectKinds(&cnpgv1.Backup{})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(gvks).To(ContainElement(HaveField("Group", cnpgv1.SchemeGroupVersion.Group)))
|
||||||
|
|
||||||
|
gvks, _, err = s.ObjectKinds(&cnpgv1.ScheduledBackup{})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(gvks).To(ContainElement(HaveField("Group", cnpgv1.SchemeGroupVersion.Group)))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should register CNPG types under a custom group", func() {
|
||||||
|
viper.Set("custom-cnpg-group", "mycompany.io")
|
||||||
|
|
||||||
|
AddCNPGToScheme(context.Background(), s)
|
||||||
|
|
||||||
|
gvks, _, err := s.ObjectKinds(&cnpgv1.Cluster{})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(gvks).To(ContainElement(schema.GroupVersionKind{
|
||||||
|
Group: "mycompany.io",
|
||||||
|
Version: cnpgv1.SchemeGroupVersion.Version,
|
||||||
|
Kind: "Cluster",
|
||||||
|
}))
|
||||||
|
// The default group must not be registered
|
||||||
|
Expect(s.Recognizes(schema.GroupVersionKind{
|
||||||
|
Group: cnpgv1.SchemeGroupVersion.Group,
|
||||||
|
Version: cnpgv1.SchemeGroupVersion.Version,
|
||||||
|
Kind: "Cluster",
|
||||||
|
})).To(BeFalse())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should register CNPG types under a custom version", func() {
|
||||||
|
viper.Set("custom-cnpg-version", "v2")
|
||||||
|
|
||||||
|
AddCNPGToScheme(context.Background(), s)
|
||||||
|
|
||||||
|
gvks, _, err := s.ObjectKinds(&cnpgv1.Cluster{})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(gvks).To(ContainElement(schema.GroupVersionKind{
|
||||||
|
Group: cnpgv1.SchemeGroupVersion.Group,
|
||||||
|
Version: "v2",
|
||||||
|
Kind: "Cluster",
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should register CNPG types under both a custom group and custom version", func() {
|
||||||
|
viper.Set("custom-cnpg-group", "mycompany.io")
|
||||||
|
viper.Set("custom-cnpg-version", "v2")
|
||||||
|
|
||||||
|
AddCNPGToScheme(context.Background(), s)
|
||||||
|
|
||||||
|
gvks, _, err := s.ObjectKinds(&cnpgv1.Cluster{})
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
Expect(gvks).To(ContainElement(schema.GroupVersionKind{
|
||||||
|
Group: "mycompany.io",
|
||||||
|
Version: "v2",
|
||||||
|
Kind: "Cluster",
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
})
|
||||||
22
internal/scheme/doc.go
Normal file
22
internal/scheme/doc.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Package scheme provides utilities for building runtime schemes
|
||||||
|
// with support for custom CNPG API groups.
|
||||||
|
package scheme
|
||||||
32
internal/scheme/suite_test.go
Normal file
32
internal/scheme/suite_test.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package scheme
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestScheme(t *testing.T) {
|
||||||
|
RegisterFailHandler(Fail)
|
||||||
|
RunSpecs(t, "Scheme Suite")
|
||||||
|
}
|
||||||
@ -37,6 +37,7 @@ import (
|
|||||||
"github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/kustomize"
|
"github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/kustomize"
|
||||||
|
|
||||||
_ "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/tests/backup"
|
_ "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/tests/backup"
|
||||||
|
_ "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/tests/credentialrotation"
|
||||||
_ "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/tests/replicacluster"
|
_ "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/tests/replicacluster"
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo/v2"
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
|||||||
@ -0,0 +1,171 @@
|
|||||||
|
/*
|
||||||
|
Copyright © contributors to CloudNativePG, established as
|
||||||
|
CloudNativePG a Series of LF Projects, LLC.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
package credentialrotation
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cloudnativepgv1 "github.com/cloudnative-pg/api/pkg/api/v1"
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
rbacv1 "k8s.io/api/rbac/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/types"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/metadata"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/internal/cnpgi/operator/specs"
|
||||||
|
internalClient "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/client"
|
||||||
|
internalCluster "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/cluster"
|
||||||
|
nmsp "github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/namespace"
|
||||||
|
"github.com/cloudnative-pg/plugin-barman-cloud/test/e2e/internal/objectstore"
|
||||||
|
|
||||||
|
. "github.com/onsi/ginkgo/v2"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
clusterName = "source"
|
||||||
|
objectStoreName = "source"
|
||||||
|
oldSecretName = "minio"
|
||||||
|
newSecretName = "minio-rotated"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Credential rotation", func() {
|
||||||
|
var namespace *corev1.Namespace
|
||||||
|
var cl client.Client
|
||||||
|
|
||||||
|
BeforeEach(func(ctx SpecContext) {
|
||||||
|
var err error
|
||||||
|
cl, _, err = internalClient.NewClient()
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
namespace, err = nmsp.CreateUniqueNamespace(ctx, cl, "cred-rotation")
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func(ctx SpecContext) {
|
||||||
|
Expect(cl.Delete(ctx, namespace)).To(Succeed())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("should update the Role when the ObjectStore secret reference changes", func(ctx SpecContext) {
|
||||||
|
By("starting the ObjectStore deployment")
|
||||||
|
resources := objectstore.NewMinioObjectStoreResources(namespace.Name, oldSecretName)
|
||||||
|
Expect(resources.Create(ctx, cl)).To(Succeed())
|
||||||
|
|
||||||
|
By("creating the ObjectStore")
|
||||||
|
store := objectstore.NewMinioObjectStore(namespace.Name, objectStoreName, oldSecretName)
|
||||||
|
Expect(cl.Create(ctx, store)).To(Succeed())
|
||||||
|
|
||||||
|
By("creating the Cluster")
|
||||||
|
cluster := newCluster(namespace.Name)
|
||||||
|
Expect(cl.Create(ctx, cluster)).To(Succeed())
|
||||||
|
|
||||||
|
By("waiting for the Cluster to be ready")
|
||||||
|
Eventually(func(g Gomega) {
|
||||||
|
g.Expect(cl.Get(ctx, types.NamespacedName{
|
||||||
|
Name: cluster.Name,
|
||||||
|
Namespace: cluster.Namespace,
|
||||||
|
}, cluster)).To(Succeed())
|
||||||
|
g.Expect(internalCluster.IsReady(*cluster)).To(BeTrue())
|
||||||
|
}).WithTimeout(10 * time.Minute).WithPolling(10 * time.Second).Should(Succeed())
|
||||||
|
|
||||||
|
roleKey := types.NamespacedName{
|
||||||
|
Name: specs.GetRBACName(clusterName),
|
||||||
|
Namespace: namespace.Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
By("verifying the Role has the cluster label and references the original secret")
|
||||||
|
var role rbacv1.Role
|
||||||
|
Expect(cl.Get(ctx, roleKey, &role)).To(Succeed())
|
||||||
|
Expect(role.Labels).To(HaveKeyWithValue(metadata.ClusterLabelName, clusterName))
|
||||||
|
Expect(secretNamesFromRole(&role)).To(ContainElement(oldSecretName))
|
||||||
|
|
||||||
|
By("creating a new secret with the same credentials")
|
||||||
|
newSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: newSecretName,
|
||||||
|
Namespace: namespace.Name,
|
||||||
|
},
|
||||||
|
Data: map[string][]byte{
|
||||||
|
"ACCESS_KEY_ID": []byte("minio"),
|
||||||
|
"ACCESS_SECRET_KEY": []byte("minio123"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
Expect(cl.Create(ctx, newSecret)).To(Succeed())
|
||||||
|
|
||||||
|
By("updating the ObjectStore to reference the new secret")
|
||||||
|
Expect(cl.Get(ctx, types.NamespacedName{
|
||||||
|
Name: objectStoreName,
|
||||||
|
Namespace: namespace.Name,
|
||||||
|
}, store)).To(Succeed())
|
||||||
|
store.Spec.Configuration.BarmanCredentials.AWS.AccessKeyIDReference.Name = newSecretName
|
||||||
|
store.Spec.Configuration.BarmanCredentials.AWS.SecretAccessKeyReference.Name = newSecretName
|
||||||
|
Expect(cl.Update(ctx, store)).To(Succeed())
|
||||||
|
|
||||||
|
By("waiting for the Role to reference the new secret")
|
||||||
|
Eventually(func(g Gomega) {
|
||||||
|
g.Expect(cl.Get(ctx, roleKey, &role)).To(Succeed())
|
||||||
|
g.Expect(secretNamesFromRole(&role)).To(ContainElement(newSecretName))
|
||||||
|
g.Expect(secretNamesFromRole(&role)).NotTo(ContainElement(oldSecretName))
|
||||||
|
}).WithTimeout(3 * time.Minute).WithPolling(5 * time.Second).Should(Succeed())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
func newCluster(namespace string) *cloudnativepgv1.Cluster {
|
||||||
|
return &cloudnativepgv1.Cluster{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Cluster",
|
||||||
|
APIVersion: "postgresql.cnpg.io/v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: clusterName,
|
||||||
|
Namespace: namespace,
|
||||||
|
},
|
||||||
|
Spec: cloudnativepgv1.ClusterSpec{
|
||||||
|
Instances: 1,
|
||||||
|
ImagePullPolicy: corev1.PullAlways,
|
||||||
|
Plugins: []cloudnativepgv1.PluginConfiguration{
|
||||||
|
{
|
||||||
|
Name: "barman-cloud.cloudnative-pg.io",
|
||||||
|
Parameters: map[string]string{
|
||||||
|
"barmanObjectName": objectStoreName,
|
||||||
|
},
|
||||||
|
IsWALArchiver: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
StorageConfiguration: cloudnativepgv1.StorageConfiguration{
|
||||||
|
Size: "1Gi",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func secretNamesFromRole(role *rbacv1.Role) []string {
|
||||||
|
for _, rule := range role.Rules {
|
||||||
|
if len(rule.APIGroups) == 1 &&
|
||||||
|
rule.APIGroups[0] == "" &&
|
||||||
|
len(rule.Resources) == 1 &&
|
||||||
|
rule.Resources[0] == "secrets" {
|
||||||
|
return rule.ResourceNames
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@ -3471,9 +3471,9 @@ balanced-match@^1.0.0:
|
|||||||
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
|
||||||
|
|
||||||
baseline-browser-mapping@^2.10.12:
|
baseline-browser-mapping@^2.10.12:
|
||||||
version "2.10.17"
|
version "2.10.18"
|
||||||
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.17.tgz#435c101835c314c2d89d768795e1ea79941fafd3"
|
resolved "https://registry.yarnpkg.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.18.tgz#565745085ba7743af7d4072707ad132db3a5a42f"
|
||||||
integrity sha512-HdrkN8eVG2CXxeifv/VdJ4A4RSra1DTW8dc/hdxzhGHN8QePs6gKaWM9pHPcpCoxYZJuOZ8drHmbdpLHjCYjLA==
|
integrity sha512-VSnGQAOLtP5mib/DPyg2/t+Tlv65NTBz83BJBJvmLVHHuKJVaDOBvJJykiT5TR++em5nfAySPccDZDa4oSrn8A==
|
||||||
|
|
||||||
batch@0.6.1:
|
batch@0.6.1:
|
||||||
version "0.6.1"
|
version "0.6.1"
|
||||||
@ -3550,9 +3550,9 @@ boxen@^7.0.0:
|
|||||||
wrap-ansi "^8.1.0"
|
wrap-ansi "^8.1.0"
|
||||||
|
|
||||||
brace-expansion@^1.1.7:
|
brace-expansion@^1.1.7:
|
||||||
version "1.1.13"
|
version "1.1.14"
|
||||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.13.tgz#d37875c01dc9eff988dd49d112a57cb67b54efe6"
|
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.14.tgz#d9de602370d91347cd9ddad1224d4fd701eb348b"
|
||||||
integrity sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==
|
integrity sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==
|
||||||
dependencies:
|
dependencies:
|
||||||
balanced-match "^1.0.0"
|
balanced-match "^1.0.0"
|
||||||
concat-map "0.0.1"
|
concat-map "0.0.1"
|
||||||
@ -4476,9 +4476,9 @@ ee-first@1.1.1:
|
|||||||
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
||||||
|
|
||||||
electron-to-chromium@^1.5.328:
|
electron-to-chromium@^1.5.328:
|
||||||
version "1.5.334"
|
version "1.5.335"
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.334.tgz#1e3fdd8d014852104eb8e632e760fb364db7dd0e"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.335.tgz#0b957cea44ef86795c227c616d16b4803d119daa"
|
||||||
integrity sha512-mgjZAz7Jyx1SRCwEpy9wefDS7GvNPazLthHg8eQMJ76wBdGQQDW33TCrUTvQ4wzpmOrv2zrFoD3oNufMdyMpog==
|
integrity sha512-q9n5T4BR4Xwa2cwbrwcsDJtHD/enpQ5S1xF1IAtdqf5AAgqDFmR/aakqH3ChFdqd/QXJhS3rnnXFtexU7rax6Q==
|
||||||
|
|
||||||
emoji-regex@^8.0.0:
|
emoji-regex@^8.0.0:
|
||||||
version "8.0.0"
|
version "8.0.0"
|
||||||
@ -8248,10 +8248,11 @@ resolve-pathname@^3.0.0:
|
|||||||
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
|
integrity sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==
|
||||||
|
|
||||||
resolve@^1.22.11:
|
resolve@^1.22.11:
|
||||||
version "1.22.11"
|
version "1.22.12"
|
||||||
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.11.tgz#aad857ce1ffb8bfa9b0b1ac29f1156383f68c262"
|
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.12.tgz#f5b2a680897c69c238a13cd16b15671f8b73549f"
|
||||||
integrity sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==
|
integrity sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
es-errors "^1.3.0"
|
||||||
is-core-module "^2.16.1"
|
is-core-module "^2.16.1"
|
||||||
path-parse "^1.0.7"
|
path-parse "^1.0.7"
|
||||||
supports-preserve-symlinks-flag "^1.0.0"
|
supports-preserve-symlinks-flag "^1.0.0"
|
||||||
@ -8951,9 +8952,9 @@ undici-types@~7.19.0:
|
|||||||
integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==
|
integrity sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==
|
||||||
|
|
||||||
undici@^7.19.0:
|
undici@^7.19.0:
|
||||||
version "7.24.7"
|
version "7.24.8"
|
||||||
resolved "https://registry.yarnpkg.com/undici/-/undici-7.24.7.tgz#af9535341bbe80625ca403a02418477a5c6a8760"
|
resolved "https://registry.yarnpkg.com/undici/-/undici-7.24.8.tgz#8207d06a68955d4420e50468f2749e940b3eb4cf"
|
||||||
integrity sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==
|
integrity sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==
|
||||||
|
|
||||||
unicode-canonical-property-names-ecmascript@^2.0.0:
|
unicode-canonical-property-names-ecmascript@^2.0.0:
|
||||||
version "2.0.1"
|
version "2.0.1"
|
||||||
@ -9269,9 +9270,9 @@ webpack-sources@^3.3.4:
|
|||||||
integrity sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==
|
integrity sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==
|
||||||
|
|
||||||
webpack@^5.88.1, webpack@^5.95.0:
|
webpack@^5.88.1, webpack@^5.95.0:
|
||||||
version "5.106.0"
|
version "5.106.1"
|
||||||
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.106.0.tgz#ee374da5573eef1e47b2650d6be8e40fb928d697"
|
resolved "https://registry.yarnpkg.com/webpack/-/webpack-5.106.1.tgz#0a3eeb43a50e4f67fbecd206e1e6fc2c89fc2b6f"
|
||||||
integrity sha512-Pkx5joZ9RrdgO5LBkyX1L2ZAJeK/Taz3vqZ9CbcP0wS5LEMx5QkKsEwLl29QJfihZ+DKRBFldzy1O30pJ1MDpA==
|
integrity sha512-EW8af29ak8Oaf4T8k8YsajjrDBDYgnKZ5er6ljWFJsXABfTNowQfvHLftwcepVgdz+IoLSdEAbBiM9DFXoll9w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/eslint-scope" "^3.7.7"
|
"@types/eslint-scope" "^3.7.7"
|
||||||
"@types/estree" "^1.0.8"
|
"@types/estree" "^1.0.8"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user