mirror of
https://github.com/zalando/postgres-operator.git
synced 2026-10-01 01:18:22 +02:00
drop deprecated fields from Postgresql CRD (#3106)
* remove deprecated fields from postgresql CRD --------- Co-authored-by: ida-novindasari_zse <ida.novindasari@zalando.de> Co-authored-by: Mikkel Oscar Lyderik Larsen <mikkeloscar@users.noreply.github.com> Co-authored-by: idanovinda <idanovinda@gmail.com>
This commit is contained in:
co-authored by
ida-novindasari_zse
Mikkel Oscar Lyderik Larsen
idanovinda
parent
a3d17c12d2
commit
18e359b995
File diff suppressed because it is too large
Load Diff
@@ -76,12 +76,6 @@ type PostgresSpec struct {
|
||||
EnableReplicaPoolerNodePort *bool `json:"enableReplicaPoolerNodePort,omitempty"`
|
||||
ReplicaPoolerNodePort *int32 `json:"replicaPoolerNodePort,omitempty"`
|
||||
|
||||
// deprecated load balancer settings maintained for backward compatibility
|
||||
// see "Load balancers" operator docs
|
||||
UseLoadBalancer *bool `json:"useLoadBalancer,omitempty"`
|
||||
// deprecated
|
||||
ReplicaLoadBalancer *bool `json:"replicaLoadBalancer,omitempty"`
|
||||
|
||||
// load balancers' source ranges are the same for master and replica services
|
||||
// +nullable
|
||||
// +kubebuilder:validation:items:Pattern=`^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$`
|
||||
@@ -130,11 +124,6 @@ type PostgresSpec struct {
|
||||
AdditionalVolumes []AdditionalVolume `json:"additionalVolumes,omitempty"`
|
||||
Streams []Stream `json:"streams,omitempty"`
|
||||
Env []v1.EnvVar `json:"env,omitempty"`
|
||||
|
||||
// deprecated
|
||||
InitContainersOld []v1.Container `json:"init_containers,omitempty"`
|
||||
// deprecated
|
||||
PodPriorityClassNameOld string `json:"pod_priority_class_name,omitempty"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zalando/postgres-operator/pkg/util"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
@@ -178,7 +177,8 @@ var unmarshalCluster = []struct {
|
||||
"metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(),
|
||||
},
|
||||
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":"Invalid"}`),
|
||||
err: nil},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
about: "example with /status subresource",
|
||||
in: []byte(`{
|
||||
@@ -199,156 +199,8 @@ var unmarshalCluster = []struct {
|
||||
"metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": 100}}`), &tmp).Error(),
|
||||
},
|
||||
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":null},"status":{"PostgresClusterStatus":"Invalid"}}`),
|
||||
err: nil},
|
||||
{
|
||||
about: "example with detailed input manifest and deprecated pod_priority_class_name -> podPriorityClassName",
|
||||
in: []byte(`{
|
||||
"kind": "Postgresql",
|
||||
"apiVersion": "acid.zalan.do/v1",
|
||||
"metadata": {
|
||||
"name": "acid-testcluster1"
|
||||
},
|
||||
"spec": {
|
||||
"teamId": "acid",
|
||||
"pod_priority_class_name": "spilo-pod-priority",
|
||||
"volume": {
|
||||
"size": "5Gi",
|
||||
"storageClass": "SSD",
|
||||
"subPath": "subdir"
|
||||
},
|
||||
"numberOfInstances": 2,
|
||||
"users": {
|
||||
"zalando": [
|
||||
"superuser",
|
||||
"createdb"
|
||||
]
|
||||
},
|
||||
"allowedSourceRanges": [
|
||||
"127.0.0.1/32"
|
||||
],
|
||||
"postgresql": {
|
||||
"version": "18",
|
||||
"parameters": {
|
||||
"shared_buffers": "32MB",
|
||||
"max_connections": "10",
|
||||
"log_statement": "all"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"requests": {
|
||||
"cpu": "10m",
|
||||
"memory": "50Mi"
|
||||
},
|
||||
"limits": {
|
||||
"cpu": "300m",
|
||||
"memory": "3000Mi"
|
||||
}
|
||||
},
|
||||
"clone" : {
|
||||
"cluster": "acid-batman"
|
||||
},
|
||||
"enableShmVolume": false,
|
||||
"patroni": {
|
||||
"initdb": {
|
||||
"encoding": "UTF8",
|
||||
"locale": "en_US.UTF-8",
|
||||
"data-checksums": "true"
|
||||
},
|
||||
"pg_hba": [
|
||||
"hostssl all all 0.0.0.0/0 md5",
|
||||
"host all all 0.0.0.0/0 md5"
|
||||
],
|
||||
"ttl": 30,
|
||||
"loop_wait": 10,
|
||||
"retry_timeout": 10,
|
||||
"maximum_lag_on_failover": 33554432,
|
||||
"slots" : {
|
||||
"permanent_logical_1" : {
|
||||
"type" : "logical",
|
||||
"database" : "foo",
|
||||
"plugin" : "pgoutput"
|
||||
}
|
||||
}
|
||||
},
|
||||
"maintenanceWindows": [
|
||||
"Mon:01:00-06:00",
|
||||
"Sat:00:00-04:00",
|
||||
"05:00-05:15"
|
||||
]
|
||||
}
|
||||
}`),
|
||||
out: Postgresql{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: "Postgresql",
|
||||
APIVersion: "acid.zalan.do/v1",
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "acid-testcluster1",
|
||||
},
|
||||
Spec: PostgresSpec{
|
||||
PostgresqlParam: PostgresqlParam{
|
||||
PgVersion: "18",
|
||||
Parameters: map[string]string{
|
||||
"shared_buffers": "32MB",
|
||||
"max_connections": "10",
|
||||
"log_statement": "all",
|
||||
},
|
||||
},
|
||||
PodPriorityClassNameOld: "spilo-pod-priority",
|
||||
Volume: Volume{
|
||||
Size: "5Gi",
|
||||
StorageClass: "SSD",
|
||||
SubPath: "subdir",
|
||||
},
|
||||
ShmVolume: util.False(),
|
||||
Patroni: Patroni{
|
||||
InitDB: map[string]string{
|
||||
"encoding": "UTF8",
|
||||
"locale": "en_US.UTF-8",
|
||||
"data-checksums": "true",
|
||||
},
|
||||
PgHba: []string{"hostssl all all 0.0.0.0/0 md5", "host all all 0.0.0.0/0 md5"},
|
||||
TTL: 30,
|
||||
LoopWait: 10,
|
||||
RetryTimeout: 10,
|
||||
MaximumLagOnFailover: 33554432,
|
||||
Slots: map[string]map[string]string{"permanent_logical_1": {"type": "logical", "database": "foo", "plugin": "pgoutput"}},
|
||||
},
|
||||
Resources: &Resources{
|
||||
ResourceRequests: ResourceDescription{CPU: stringToPointer("10m"), Memory: stringToPointer("50Mi")},
|
||||
ResourceLimits: ResourceDescription{CPU: stringToPointer("300m"), Memory: stringToPointer("3000Mi")},
|
||||
},
|
||||
|
||||
TeamID: "acid",
|
||||
AllowedSourceRanges: []string{"127.0.0.1/32"},
|
||||
NumberOfInstances: 2,
|
||||
Users: map[string]UserFlags{"zalando": {"superuser", "createdb"}},
|
||||
MaintenanceWindows: []MaintenanceWindow{{
|
||||
Everyday: false,
|
||||
Weekday: time.Monday,
|
||||
StartTime: mustParseTime("01:00"),
|
||||
EndTime: mustParseTime("06:00"),
|
||||
}, {
|
||||
Everyday: false,
|
||||
Weekday: time.Saturday,
|
||||
StartTime: mustParseTime("00:00"),
|
||||
EndTime: mustParseTime("04:00"),
|
||||
},
|
||||
{
|
||||
Everyday: true,
|
||||
Weekday: time.Sunday,
|
||||
StartTime: mustParseTime("05:00"),
|
||||
EndTime: mustParseTime("05:15"),
|
||||
},
|
||||
},
|
||||
Clone: &CloneDescription{
|
||||
ClusterName: "acid-batman",
|
||||
},
|
||||
},
|
||||
Error: "",
|
||||
},
|
||||
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"18","parameters":{"log_statement":"all","max_connections":"10","shared_buffers":"32MB"}},"pod_priority_class_name":"spilo-pod-priority","volume":{"size":"5Gi","storageClass":"SSD", "subPath": "subdir"},"enableShmVolume":false,"patroni":{"initdb":{"data-checksums":"true","encoding":"UTF8","locale":"en_US.UTF-8"},"pg_hba":["hostssl all all 0.0.0.0/0 md5","host all all 0.0.0.0/0 md5"],"ttl":30,"loop_wait":10,"retry_timeout":10,"maximum_lag_on_failover":33554432,"slots":{"permanent_logical_1":{"database":"foo","plugin":"pgoutput","type":"logical"}}},"resources":{"requests":{"cpu":"10m","memory":"50Mi"},"limits":{"cpu":"300m","memory":"3000Mi"}},"teamId":"acid","allowedSourceRanges":["127.0.0.1/32"],"numberOfInstances":2,"users":{"zalando":["superuser","createdb"]},"maintenanceWindows":["Mon:01:00-06:00","Sat:00:00-04:00","05:00-05:15"],"clone":{"cluster":"acid-batman"}},"status":{"PostgresClusterStatus":""}}`),
|
||||
err: nil},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
about: "example with clone",
|
||||
in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": "acid", "clone": {"cluster": "team-batman"}}}`),
|
||||
@@ -369,7 +221,8 @@ var unmarshalCluster = []struct {
|
||||
Error: "",
|
||||
},
|
||||
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"clone":{"cluster":"team-batman"}},"status":{"PostgresClusterStatus":""}}`),
|
||||
err: nil},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
about: "standby example",
|
||||
in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1","metadata": {"name": "acid-testcluster1"}, "spec": {"teamId": "acid", "standby": {"s3_wal_path": "s3://custom/path/to/bucket/"}}}`),
|
||||
@@ -390,7 +243,8 @@ var unmarshalCluster = []struct {
|
||||
Error: "",
|
||||
},
|
||||
marshal: []byte(`{"kind":"Postgresql","apiVersion":"acid.zalan.do/v1","metadata":{"name":"acid-testcluster1","creationTimestamp":null},"spec":{"postgresql":{"version":"","parameters":null},"volume":{"size":"","storageClass":""},"patroni":{"initdb":null,"pg_hba":null,"ttl":0,"loop_wait":0,"retry_timeout":0,"maximum_lag_on_failover":0,"slots":null},"teamId":"acid","allowedSourceRanges":null,"numberOfInstances":0,"users":null,"standby":{"s3_wal_path":"s3://custom/path/to/bucket/"}},"status":{"PostgresClusterStatus":""}}`),
|
||||
err: nil},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
about: "expect error on malformatted JSON",
|
||||
in: []byte(`{"kind": "Postgresql","apiVersion": "acid.zalan.do/v1"`),
|
||||
|
||||
@@ -848,16 +848,6 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
|
||||
*out = new(int32)
|
||||
**out = **in
|
||||
}
|
||||
if in.UseLoadBalancer != nil {
|
||||
in, out := &in.UseLoadBalancer, &out.UseLoadBalancer
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.ReplicaLoadBalancer != nil {
|
||||
in, out := &in.ReplicaLoadBalancer, &out.ReplicaLoadBalancer
|
||||
*out = new(bool)
|
||||
**out = **in
|
||||
}
|
||||
if in.AllowedSourceRanges != nil {
|
||||
in, out := &in.AllowedSourceRanges, &out.AllowedSourceRanges
|
||||
*out = make([]string, len(*in))
|
||||
@@ -1026,13 +1016,6 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
if in.InitContainersOld != nil {
|
||||
in, out := &in.InitContainersOld, &out.InitContainersOld
|
||||
*out = make([]corev1.Container, len(*in))
|
||||
for i := range *in {
|
||||
(*in)[i].DeepCopyInto(&(*out)[i])
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -1084,7 +1084,7 @@ func (c *Cluster) Update(oldSpec, newSpec *acidv1.Postgresql) error {
|
||||
|
||||
// Patroni service and endpoints / config maps
|
||||
if err := c.syncPatroniResources(); err != nil {
|
||||
c.logger.Errorf("could not sync services: %v", err)
|
||||
c.logger.Errorf("could not sync Patroni resources: %v", err)
|
||||
updateFailed = true
|
||||
}
|
||||
|
||||
|
||||
@@ -1340,28 +1340,6 @@ func (c *Cluster) generateStatefulSet(spec *acidv1.PostgresSpec) (*appsv1.Statef
|
||||
}
|
||||
}
|
||||
|
||||
// backward compatible check for InitContainers
|
||||
if spec.InitContainersOld != nil {
|
||||
msg := "manifest parameter init_containers is deprecated."
|
||||
if spec.InitContainers == nil {
|
||||
c.logger.Warningf("%s Consider using initContainers instead.", msg)
|
||||
spec.InitContainers = spec.InitContainersOld
|
||||
} else {
|
||||
c.logger.Warningf("%s Only value from initContainers is used", msg)
|
||||
}
|
||||
}
|
||||
|
||||
// backward compatible check for PodPriorityClassName
|
||||
if spec.PodPriorityClassNameOld != "" {
|
||||
msg := "manifest parameter pod_priority_class_name is deprecated."
|
||||
if spec.PodPriorityClassName == "" {
|
||||
c.logger.Warningf("%s Consider using podPriorityClassName instead.", msg)
|
||||
spec.PodPriorityClassName = spec.PodPriorityClassNameOld
|
||||
} else {
|
||||
c.logger.Warningf("%s Only value from podPriorityClassName is used", msg)
|
||||
}
|
||||
}
|
||||
|
||||
spiloConfiguration, err := generateSpiloJSONConfiguration(&spec.PostgresqlParam, &spec.Patroni, &c.OpConfig, c.logger)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not generate Spilo JSON configuration: %v", err)
|
||||
|
||||
@@ -213,22 +213,10 @@ func (c *Controller) processEvent(event ClusterEvent, isInInitialList bool) {
|
||||
}
|
||||
lg.Debugf("observed cluster status %s, running sync scan to repair the cluster", lastOperationStatus)
|
||||
event.EventType = EventSync
|
||||
}
|
||||
|
||||
if event.EventType == EventAdd || event.EventType == EventUpdate || event.EventType == EventSync {
|
||||
// handle deprecated parameters by possibly assigning their values to the new ones.
|
||||
if event.OldSpec != nil {
|
||||
c.mergeDeprecatedPostgreSQLSpecParameters(&event.OldSpec.Spec)
|
||||
}
|
||||
if event.NewSpec != nil {
|
||||
c.warnOnDeprecatedPostgreSQLSpecParameters(&event.NewSpec.Spec)
|
||||
c.mergeDeprecatedPostgreSQLSpecParameters(&event.NewSpec.Spec)
|
||||
}
|
||||
|
||||
} else if event.EventType != EventDelete {
|
||||
if err = c.submitRBACCredentials(event); err != nil {
|
||||
c.logger.Warnf("pods and/or Patroni may misfunction due to the lack of permissions: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
switch event.EventType {
|
||||
@@ -397,45 +385,6 @@ func (c *Controller) processClusterEventsQueue(idx int, stopCh <-chan struct{},
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Controller) warnOnDeprecatedPostgreSQLSpecParameters(spec *acidv1.PostgresSpec) {
|
||||
deprecate := func(deprecated, replacement string) {
|
||||
c.logger.Warningf("parameter %q is deprecated. Consider setting %q instead", deprecated, replacement)
|
||||
}
|
||||
|
||||
if spec.UseLoadBalancer != nil {
|
||||
deprecate("useLoadBalancer", "enableMasterLoadBalancer")
|
||||
}
|
||||
if spec.ReplicaLoadBalancer != nil {
|
||||
deprecate("replicaLoadBalancer", "enableReplicaLoadBalancer")
|
||||
}
|
||||
|
||||
if (spec.UseLoadBalancer != nil || spec.ReplicaLoadBalancer != nil) &&
|
||||
(spec.EnableReplicaLoadBalancer != nil || spec.EnableMasterLoadBalancer != nil) {
|
||||
c.logger.Warnf("both old and new load balancer parameters are present in the manifest, ignoring old ones")
|
||||
}
|
||||
}
|
||||
|
||||
// mergeDeprecatedPostgreSQLSpecParameters modifies the spec passed to the cluster by setting current parameter
|
||||
// values from the obsolete ones. Note: while the spec that is modified is a copy made in queueClusterEvent, it is
|
||||
// still a shallow copy, so be extra careful not to modify values pointer fields point to, but copy them instead.
|
||||
func (c *Controller) mergeDeprecatedPostgreSQLSpecParameters(spec *acidv1.PostgresSpec) *acidv1.PostgresSpec {
|
||||
if (spec.UseLoadBalancer != nil || spec.ReplicaLoadBalancer != nil) &&
|
||||
(spec.EnableReplicaLoadBalancer == nil && spec.EnableMasterLoadBalancer == nil) {
|
||||
if spec.UseLoadBalancer != nil {
|
||||
spec.EnableMasterLoadBalancer = new(bool)
|
||||
*spec.EnableMasterLoadBalancer = *spec.UseLoadBalancer
|
||||
}
|
||||
if spec.ReplicaLoadBalancer != nil {
|
||||
spec.EnableReplicaLoadBalancer = new(bool)
|
||||
*spec.EnableReplicaLoadBalancer = *spec.ReplicaLoadBalancer
|
||||
}
|
||||
}
|
||||
spec.ReplicaLoadBalancer = nil
|
||||
spec.UseLoadBalancer = nil
|
||||
|
||||
return spec
|
||||
}
|
||||
|
||||
func (c *Controller) queueClusterEvent(informerOldSpec, informerNewSpec *acidv1.Postgresql, eventType EventType) {
|
||||
var (
|
||||
uid types.UID
|
||||
|
||||
@@ -64,35 +64,6 @@ func TestControllerOwnershipOnPostgresql(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeDeprecatedPostgreSQLSpecParameters(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
in *acidv1.PostgresSpec
|
||||
out *acidv1.PostgresSpec
|
||||
error string
|
||||
}{
|
||||
{
|
||||
"Check that old parameters propagate values to the new ones",
|
||||
&acidv1.PostgresSpec{UseLoadBalancer: &True, ReplicaLoadBalancer: &True},
|
||||
&acidv1.PostgresSpec{UseLoadBalancer: nil, ReplicaLoadBalancer: nil,
|
||||
EnableMasterLoadBalancer: &True, EnableReplicaLoadBalancer: &True},
|
||||
"New parameters should be set from the values of old ones",
|
||||
},
|
||||
{
|
||||
"Check that new parameters are not set when both old and new ones are present",
|
||||
&acidv1.PostgresSpec{UseLoadBalancer: &True, EnableMasterLoadBalancer: &False},
|
||||
&acidv1.PostgresSpec{UseLoadBalancer: nil, EnableMasterLoadBalancer: &False},
|
||||
"New parameters should remain unchanged when both old and new are present",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
result := postgresqlTestController.mergeDeprecatedPostgreSQLSpecParameters(tt.in)
|
||||
if !reflect.DeepEqual(result, tt.out) {
|
||||
t.Errorf("%s: %v", tt.name, tt.error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetsClusterDeleteAnnotations(t *testing.T) {
|
||||
// set delete annotations in configuration
|
||||
postgresqlTestController.opConfig.DeleteAnnotationDateKey = "delete-date"
|
||||
|
||||
Reference in New Issue
Block a user