mirror of
https://github.com/cloudnative-pg/plugin-barman-cloud.git
synced 2026-03-10 12:42:20 +01:00
Compare commits
5 Commits
d297c0fbea
...
d8adca09ac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d8adca09ac | ||
|
|
5bc006b035 | ||
|
|
4f5b407c0f | ||
|
|
6a55a361a3 | ||
|
|
62b579101f |
@ -85,9 +85,13 @@ tasks:
|
|||||||
env:
|
env:
|
||||||
# renovate: datasource=git-refs depName=crd-gen-refs lookupName=https://github.com/cloudnative-pg/daggerverse currentValue=main
|
# renovate: datasource=git-refs depName=crd-gen-refs lookupName=https://github.com/cloudnative-pg/daggerverse currentValue=main
|
||||||
DAGGER_CRDGENREF_SHA: ee59e34a99940e45f87a16177b1d640975b05b74
|
DAGGER_CRDGENREF_SHA: ee59e34a99940e45f87a16177b1d640975b05b74
|
||||||
|
# renovate: datasource=go depName=github.com/elastic/crd-ref-docs
|
||||||
|
CRDREFDOCS_VERSION: v0.2.0
|
||||||
cmds:
|
cmds:
|
||||||
- >
|
- >
|
||||||
GITHUB_REF= dagger -s call -m github.com/cloudnative-pg/daggerverse/crd-ref-docs@${DAGGER_CRDGENREF_SHA} generate
|
GITHUB_REF= dagger -s call -m github.com/cloudnative-pg/daggerverse/crd-ref-docs@${DAGGER_CRDGENREF_SHA}
|
||||||
|
--version ${CRDREFDOCS_VERSION}
|
||||||
|
generate
|
||||||
--src .
|
--src .
|
||||||
--source-path api/v1
|
--source-path api/v1
|
||||||
--config-file hack/crd-gen-refs/config.yaml
|
--config-file hack/crd-gen-refs/config.yaml
|
||||||
|
|||||||
@ -36,6 +36,9 @@ import (
|
|||||||
// DefaultTTLSeconds is the default TTL in seconds of cache entries
|
// DefaultTTLSeconds is the default TTL in seconds of cache entries
|
||||||
const DefaultTTLSeconds = 10
|
const DefaultTTLSeconds = 10
|
||||||
|
|
||||||
|
// DefaultCleanupIntervalSeconds is the default interval in seconds for cache cleanup
|
||||||
|
const DefaultCleanupIntervalSeconds = 30
|
||||||
|
|
||||||
type cachedEntry struct {
|
type cachedEntry struct {
|
||||||
entry client.Object
|
entry client.Object
|
||||||
fetchUnixTime int64
|
fetchUnixTime int64
|
||||||
@ -49,18 +52,30 @@ func (e *cachedEntry) isExpired() bool {
|
|||||||
// ExtendedClient is an extended client that is capable of caching multiple secrets without relying on informers
|
// ExtendedClient is an extended client that is capable of caching multiple secrets without relying on informers
|
||||||
type ExtendedClient struct {
|
type ExtendedClient struct {
|
||||||
client.Client
|
client.Client
|
||||||
cachedObjects []cachedEntry
|
cachedObjects []cachedEntry
|
||||||
mux *sync.Mutex
|
mux *sync.Mutex
|
||||||
|
cleanupInterval time.Duration
|
||||||
|
cleanupDone chan struct{} // Signals when cleanup routine exits
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewExtendedClient returns an extended client capable of caching secrets on the 'Get' operation
|
// NewExtendedClient returns an extended client capable of caching secrets on the 'Get' operation.
|
||||||
|
// It starts a background goroutine that periodically cleans up expired cache entries.
|
||||||
|
// The cleanup routine will stop when the provided context is cancelled.
|
||||||
func NewExtendedClient(
|
func NewExtendedClient(
|
||||||
|
ctx context.Context,
|
||||||
baseClient client.Client,
|
baseClient client.Client,
|
||||||
) client.Client {
|
) client.Client {
|
||||||
return &ExtendedClient{
|
ec := &ExtendedClient{
|
||||||
Client: baseClient,
|
Client: baseClient,
|
||||||
mux: &sync.Mutex{},
|
mux: &sync.Mutex{},
|
||||||
|
cleanupInterval: DefaultCleanupIntervalSeconds * time.Second,
|
||||||
|
cleanupDone: make(chan struct{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Start the background cleanup routine
|
||||||
|
go ec.startCleanupRoutine(ctx)
|
||||||
|
|
||||||
|
return ec
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *ExtendedClient) isObjectCached(obj client.Object) bool {
|
func (e *ExtendedClient) isObjectCached(obj client.Object) bool {
|
||||||
@ -208,3 +223,55 @@ func (e *ExtendedClient) Patch(
|
|||||||
|
|
||||||
return e.Client.Patch(ctx, obj, patch, opts...)
|
return e.Client.Patch(ctx, obj, patch, opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// startCleanupRoutine periodically removes expired entries from the cache.
|
||||||
|
// It runs until the context is cancelled.
|
||||||
|
func (e *ExtendedClient) startCleanupRoutine(ctx context.Context) {
|
||||||
|
defer close(e.cleanupDone)
|
||||||
|
contextLogger := log.FromContext(ctx).WithName("extended_client_cleanup")
|
||||||
|
ticker := time.NewTicker(e.cleanupInterval)
|
||||||
|
defer ticker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
contextLogger.Debug("stopping cache cleanup routine")
|
||||||
|
return
|
||||||
|
case <-ticker.C:
|
||||||
|
// Check context before cleanup to avoid unnecessary work during shutdown
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
e.cleanupExpiredEntries(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cleanupExpiredEntries removes all expired entries from the cache.
|
||||||
|
func (e *ExtendedClient) cleanupExpiredEntries(ctx context.Context) {
|
||||||
|
contextLogger := log.FromContext(ctx).WithName("extended_client_cleanup")
|
||||||
|
|
||||||
|
e.mux.Lock()
|
||||||
|
defer e.mux.Unlock()
|
||||||
|
|
||||||
|
initialCount := len(e.cachedObjects)
|
||||||
|
if initialCount == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new slice with only non-expired entries
|
||||||
|
validEntries := make([]cachedEntry, 0, initialCount)
|
||||||
|
for _, entry := range e.cachedObjects {
|
||||||
|
if !entry.isExpired() {
|
||||||
|
validEntries = append(validEntries, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removedCount := initialCount - len(validEntries)
|
||||||
|
if removedCount > 0 {
|
||||||
|
e.cachedObjects = validEntries
|
||||||
|
contextLogger.Debug("cleaned up expired cache entries",
|
||||||
|
"removedCount", removedCount,
|
||||||
|
"remainingCount", len(validEntries))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -20,6 +20,7 @@ SPDX-License-Identifier: Apache-2.0
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
@ -59,6 +60,7 @@ var _ = Describe("ExtendedClient Get", func() {
|
|||||||
extendedClient *ExtendedClient
|
extendedClient *ExtendedClient
|
||||||
secretInClient *corev1.Secret
|
secretInClient *corev1.Secret
|
||||||
objectStore *barmancloudv1.ObjectStore
|
objectStore *barmancloudv1.ObjectStore
|
||||||
|
cancelCtx context.CancelFunc
|
||||||
)
|
)
|
||||||
|
|
||||||
BeforeEach(func() {
|
BeforeEach(func() {
|
||||||
@ -79,7 +81,14 @@ var _ = Describe("ExtendedClient Get", func() {
|
|||||||
baseClient := fake.NewClientBuilder().
|
baseClient := fake.NewClientBuilder().
|
||||||
WithScheme(scheme).
|
WithScheme(scheme).
|
||||||
WithObjects(secretInClient, objectStore).Build()
|
WithObjects(secretInClient, objectStore).Build()
|
||||||
extendedClient = NewExtendedClient(baseClient).(*ExtendedClient)
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancelCtx = cancel
|
||||||
|
extendedClient = NewExtendedClient(ctx, baseClient).(*ExtendedClient)
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
// Cancel the context to stop the cleanup routine
|
||||||
|
cancelCtx()
|
||||||
})
|
})
|
||||||
|
|
||||||
It("returns secret from cache if not expired", func(ctx SpecContext) {
|
It("returns secret from cache if not expired", func(ctx SpecContext) {
|
||||||
@ -164,3 +173,141 @@ var _ = Describe("ExtendedClient Get", func() {
|
|||||||
Expect(objectStore.GetResourceVersion()).To(Equal("from cache"))
|
Expect(objectStore.GetResourceVersion()).To(Equal("from cache"))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
var _ = Describe("ExtendedClient Cache Cleanup", func() {
|
||||||
|
var (
|
||||||
|
extendedClient *ExtendedClient
|
||||||
|
cancelCtx context.CancelFunc
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
baseClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
Build()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancelCtx = cancel
|
||||||
|
extendedClient = NewExtendedClient(ctx, baseClient).(*ExtendedClient)
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
cancelCtx()
|
||||||
|
})
|
||||||
|
|
||||||
|
It("cleans up expired entries", func(ctx SpecContext) {
|
||||||
|
// Add some expired entries
|
||||||
|
expiredSecret1 := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "expired-secret-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expiredSecret2 := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "expired-secret-2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
validSecret := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "valid-secret",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add expired entries (2 minutes ago)
|
||||||
|
addToCache(extendedClient, expiredSecret1, time.Now().Add(-2*time.Minute).Unix())
|
||||||
|
addToCache(extendedClient, expiredSecret2, time.Now().Add(-2*time.Minute).Unix())
|
||||||
|
// Add valid entry (just now)
|
||||||
|
addToCache(extendedClient, validSecret, time.Now().Unix())
|
||||||
|
|
||||||
|
Expect(extendedClient.cachedObjects).To(HaveLen(3))
|
||||||
|
|
||||||
|
// Trigger cleanup
|
||||||
|
extendedClient.cleanupExpiredEntries(ctx)
|
||||||
|
|
||||||
|
// Only the valid entry should remain
|
||||||
|
Expect(extendedClient.cachedObjects).To(HaveLen(1))
|
||||||
|
Expect(extendedClient.cachedObjects[0].entry.GetName()).To(Equal("valid-secret"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("does nothing when all entries are valid", func(ctx SpecContext) {
|
||||||
|
validSecret1 := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "valid-secret-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
validSecret2 := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "valid-secret-2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
addToCache(extendedClient, validSecret1, time.Now().Unix())
|
||||||
|
addToCache(extendedClient, validSecret2, time.Now().Unix())
|
||||||
|
|
||||||
|
Expect(extendedClient.cachedObjects).To(HaveLen(2))
|
||||||
|
|
||||||
|
// Trigger cleanup
|
||||||
|
extendedClient.cleanupExpiredEntries(ctx)
|
||||||
|
|
||||||
|
// Both entries should remain
|
||||||
|
Expect(extendedClient.cachedObjects).To(HaveLen(2))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("does nothing when cache is empty", func(ctx SpecContext) {
|
||||||
|
Expect(extendedClient.cachedObjects).To(BeEmpty())
|
||||||
|
|
||||||
|
// Trigger cleanup
|
||||||
|
extendedClient.cleanupExpiredEntries(ctx)
|
||||||
|
|
||||||
|
Expect(extendedClient.cachedObjects).To(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("removes all entries when all are expired", func(ctx SpecContext) {
|
||||||
|
expiredSecret1 := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "expired-secret-1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
expiredSecret2 := &corev1.Secret{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "expired-secret-2",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
addToCache(extendedClient, expiredSecret1, time.Now().Add(-2*time.Minute).Unix())
|
||||||
|
addToCache(extendedClient, expiredSecret2, time.Now().Add(-2*time.Minute).Unix())
|
||||||
|
|
||||||
|
Expect(extendedClient.cachedObjects).To(HaveLen(2))
|
||||||
|
|
||||||
|
// Trigger cleanup
|
||||||
|
extendedClient.cleanupExpiredEntries(ctx)
|
||||||
|
|
||||||
|
Expect(extendedClient.cachedObjects).To(BeEmpty())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("stops cleanup routine when context is cancelled", func() {
|
||||||
|
// Create a new client with a short cleanup interval for testing
|
||||||
|
baseClient := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
Build()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
ec := NewExtendedClient(ctx, baseClient).(*ExtendedClient)
|
||||||
|
ec.cleanupInterval = 10 * time.Millisecond
|
||||||
|
|
||||||
|
// Cancel the context immediately
|
||||||
|
cancel()
|
||||||
|
|
||||||
|
// Verify the cleanup routine actually stops by waiting for the done channel
|
||||||
|
select {
|
||||||
|
case <-ec.cleanupDone:
|
||||||
|
// Success: cleanup routine exited as expected
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
Fail("cleanup routine did not stop within timeout")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|||||||
@ -84,7 +84,7 @@ func Start(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
customCacheClient := extendedclient.NewExtendedClient(mgr.GetClient())
|
customCacheClient := extendedclient.NewExtendedClient(ctx, mgr.GetClient())
|
||||||
|
|
||||||
if err := mgr.Add(&CNPGI{
|
if err := mgr.Add(&CNPGI{
|
||||||
Client: customCacheClient,
|
Client: customCacheClient,
|
||||||
|
|||||||
@ -11,6 +11,17 @@
|
|||||||
],
|
],
|
||||||
rebaseWhen: 'never',
|
rebaseWhen: 'never',
|
||||||
prConcurrentLimit: 5,
|
prConcurrentLimit: 5,
|
||||||
|
// Override default ignorePaths to scan test/e2e for emulator image dependencies
|
||||||
|
// Removed: '**/test/**'
|
||||||
|
ignorePaths: [
|
||||||
|
'**/node_modules/**',
|
||||||
|
'**/bower_components/**',
|
||||||
|
'**/vendor/**',
|
||||||
|
'**/examples/**',
|
||||||
|
'**/__tests__/**',
|
||||||
|
'**/tests/**',
|
||||||
|
'**/__fixtures__/**',
|
||||||
|
],
|
||||||
lockFileMaintenance: {
|
lockFileMaintenance: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
@ -28,7 +39,7 @@
|
|||||||
{
|
{
|
||||||
customType: 'regex',
|
customType: 'regex',
|
||||||
managerFilePatterns: [
|
managerFilePatterns: [
|
||||||
'/(^Taskfile\\.yml$)/',
|
'/(^|/)Taskfile\\.yml$/',
|
||||||
],
|
],
|
||||||
matchStrings: [
|
matchStrings: [
|
||||||
'# renovate: datasource=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (?:lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?(?: currentValue=(?<currentValue>[^\\s]+?))?\\s+[A-Za-z0-9_]+?_SHA\\s*:\\s*["\']?(?<currentDigest>[a-f0-9]+?)["\']?\\s',
|
'# renovate: datasource=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (?:lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?(?: currentValue=(?<currentValue>[^\\s]+?))?\\s+[A-Za-z0-9_]+?_SHA\\s*:\\s*["\']?(?<currentDigest>[a-f0-9]+?)["\']?\\s',
|
||||||
@ -38,7 +49,16 @@
|
|||||||
{
|
{
|
||||||
customType: 'regex',
|
customType: 'regex',
|
||||||
managerFilePatterns: [
|
managerFilePatterns: [
|
||||||
'/(^docs/config\\.yaml$)/',
|
'/\\.go$/',
|
||||||
|
],
|
||||||
|
matchStrings: [
|
||||||
|
'//\\s*renovate:\\s*datasource=(?<datasource>[a-z-.]+?)\\s+depName=(?<depName>[^\\s]+?)(?:\\s+versioning=(?<versioning>[^\\s]+?))?\\s*\\n\\s*//\\s*Version:\\s*(?<currentValue>[^\\s]+?)\\s*\\n\\s*Image:\\s*"[^@]+@(?<currentDigest>sha256:[a-f0-9]+)"',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
customType: 'regex',
|
||||||
|
managerFilePatterns: [
|
||||||
|
'/(^|/)docs/config\\.yaml$/',
|
||||||
],
|
],
|
||||||
matchStrings: [
|
matchStrings: [
|
||||||
'# renovate: datasource=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (?:lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?\\s+kubernetesVersion:\\s*["\']?(?<currentValue>.+?)["\']?\\s',
|
'# renovate: datasource=(?<datasource>[a-z-.]+?) depName=(?<depName>[^\\s]+?)(?: (?:lookupName|packageName)=(?<packageName>[^\\s]+?))?(?: versioning=(?<versioning>[^\\s]+?))?(?: extractVersion=(?<extractVersion>[^\\s]+?))?\\s+kubernetesVersion:\\s*["\']?(?<currentValue>.+?)["\']?\\s',
|
||||||
|
|||||||
@ -71,8 +71,15 @@ func newAzuriteDeployment(namespace, name string) *appsv1.Deployment {
|
|||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
Name: name,
|
Name: name,
|
||||||
// TODO: renovate the image
|
// renovate: datasource=docker depName=mcr.microsoft.com/azure-storage/azurite versioning=docker
|
||||||
Image: "mcr.microsoft.com/azure-storage/azurite",
|
// Version: 3.35.0
|
||||||
|
Image: "mcr.microsoft.com/azure-storage/azurite@sha256:647c63a91102a9d8e8000aab803436e1fc85fbb285e7ce830a82ee5d6661cf37",
|
||||||
|
Args: []string{
|
||||||
|
"azurite-blob",
|
||||||
|
"--blobHost",
|
||||||
|
"0.0.0.0",
|
||||||
|
"--skipApiVersionCheck",
|
||||||
|
},
|
||||||
Ports: []corev1.ContainerPort{
|
Ports: []corev1.ContainerPort{
|
||||||
{
|
{
|
||||||
ContainerPort: 10000,
|
ContainerPort: 10000,
|
||||||
|
|||||||
@ -71,7 +71,9 @@ func newGCSDeployment(namespace, name string) *appsv1.Deployment {
|
|||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
Name: name,
|
Name: name,
|
||||||
Image: "fsouza/fake-gcs-server:latest",
|
// renovate: datasource=docker depName=fsouza/fake-gcs-server versioning=docker
|
||||||
|
// Version: 1.52.3
|
||||||
|
Image: "fsouza/fake-gcs-server@sha256:666f86b873120818b10a5e68d99401422fcf8b00c1f27fe89599c35236f48b4c",
|
||||||
Ports: []corev1.ContainerPort{
|
Ports: []corev1.ContainerPort{
|
||||||
{
|
{
|
||||||
ContainerPort: 4443,
|
ContainerPort: 4443,
|
||||||
|
|||||||
@ -71,8 +71,9 @@ func newMinioDeployment(namespace, name string) *appsv1.Deployment {
|
|||||||
Containers: []corev1.Container{
|
Containers: []corev1.Container{
|
||||||
{
|
{
|
||||||
Name: name,
|
Name: name,
|
||||||
// TODO: renovate the image
|
// renovate: datasource=docker depName=minio/minio versioning=docker
|
||||||
Image: "minio/minio:latest",
|
// Version: RELEASE.2025-09-07T16-13-09Z
|
||||||
|
Image: "minio/minio@sha256:14cea493d9a34af32f524e538b8346cf79f3321eff8e708c1e2960462bd8936e",
|
||||||
Args: []string{"server", "/data"},
|
Args: []string{"server", "/data"},
|
||||||
Ports: []corev1.ContainerPort{
|
Ports: []corev1.ContainerPort{
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user