mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 20:53:33 +00:00
apiextensions: don't prune meta fields with x-kubernetes-embedded-resource
This commit is contained in:
parent
3cd511b86c
commit
f1bc7b69a8
@ -20,11 +20,26 @@ import (
|
|||||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Prune removes object fields in obj which are not specified in s.
|
// Prune removes object fields in obj which are not specified in s. It skips TypeMeta and ObjectMeta fields
|
||||||
func Prune(obj interface{}, s *structuralschema.Structural) {
|
// if XEmbeddedResource is set to true, or for the root if root=true.
|
||||||
|
func Prune(obj interface{}, s *structuralschema.Structural, root bool) {
|
||||||
|
if root {
|
||||||
|
if s == nil {
|
||||||
|
s = &structuralschema.Structural{}
|
||||||
|
}
|
||||||
|
clone := *s
|
||||||
|
clone.XEmbeddedResource = true
|
||||||
|
s = &clone
|
||||||
|
}
|
||||||
prune(obj, s)
|
prune(obj, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var metaFields = map[string]bool{
|
||||||
|
"apiVersion": true,
|
||||||
|
"kind": true,
|
||||||
|
"metadata": true,
|
||||||
|
}
|
||||||
|
|
||||||
func prune(x interface{}, s *structuralschema.Structural) {
|
func prune(x interface{}, s *structuralschema.Structural) {
|
||||||
if s != nil && s.XPreserveUnknownFields {
|
if s != nil && s.XPreserveUnknownFields {
|
||||||
skipPrune(x, s)
|
skipPrune(x, s)
|
||||||
@ -40,6 +55,9 @@ func prune(x interface{}, s *structuralschema.Structural) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
for k, v := range x {
|
for k, v := range x {
|
||||||
|
if s.XEmbeddedResource && metaFields[k] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
prop, ok := s.Properties[k]
|
prop, ok := s.Properties[k]
|
||||||
if ok {
|
if ok {
|
||||||
prune(v, &prop)
|
prune(v, &prop)
|
||||||
@ -72,6 +90,9 @@ func skipPrune(x interface{}, s *structuralschema.Structural) {
|
|||||||
switch x := x.(type) {
|
switch x := x.(type) {
|
||||||
case map[string]interface{}:
|
case map[string]interface{}:
|
||||||
for k, v := range x {
|
for k, v := range x {
|
||||||
|
if s.XEmbeddedResource && metaFields[k] {
|
||||||
|
continue
|
||||||
|
}
|
||||||
if prop, ok := s.Properties[k]; ok {
|
if prop, ok := s.Properties[k]; ok {
|
||||||
prune(v, &prop)
|
prune(v, &prop)
|
||||||
} else {
|
} else {
|
||||||
|
@ -23,31 +23,33 @@ import (
|
|||||||
|
|
||||||
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
structuralschema "k8s.io/apiextensions-apiserver/pkg/apiserver/schema"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/util/diff"
|
||||||
"k8s.io/apimachinery/pkg/util/json"
|
"k8s.io/apimachinery/pkg/util/json"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPrune(t *testing.T) {
|
func TestPrune(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
json string
|
json string
|
||||||
schema *structuralschema.Structural
|
dontPruneMetaAtRoot bool
|
||||||
expected string
|
schema *structuralschema.Structural
|
||||||
|
expected string
|
||||||
}{
|
}{
|
||||||
{"empty", "null", nil, "null"},
|
{name: "empty", json: "null", expected: "null"},
|
||||||
{"scalar", "4", &structuralschema.Structural{}, "4"},
|
{name: "scalar", json: "4", schema: &structuralschema.Structural{}, expected: "4"},
|
||||||
{"scalar array", "[1,2]", &structuralschema.Structural{
|
{name: "scalar array", json: "[1,2]", schema: &structuralschema.Structural{
|
||||||
Items: &structuralschema.Structural{},
|
Items: &structuralschema.Structural{},
|
||||||
}, "[1,2]"},
|
}, expected: "[1,2]"},
|
||||||
{"object array", `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, &structuralschema.Structural{
|
{name: "object array", json: `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, schema: &structuralschema.Structural{
|
||||||
Items: &structuralschema.Structural{
|
Items: &structuralschema.Structural{
|
||||||
Properties: map[string]structuralschema.Structural{
|
Properties: map[string]structuralschema.Structural{
|
||||||
"a": {},
|
"a": {},
|
||||||
"c": {},
|
"c": {},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, `[{"a":1},{},{"a":1,"c":3}]`},
|
}, expected: `[{"a":1},{},{"a":1,"c":3}]`},
|
||||||
{"object array with nil schema", `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, nil, `[{},{},{}]`},
|
{name: "object array with nil schema", json: `[{"a":1},{"b":1},{"a":1,"b":2,"c":3}]`, expected: `[{},{},{}]`},
|
||||||
{"object array object", `{"array":[{"a":1},{"b":1},{"a":1,"b":2,"c":3}],"unspecified":{"a":1},"specified":{"a":1,"b":2,"c":3}}`, &structuralschema.Structural{
|
{name: "object array object", json: `{"array":[{"a":1},{"b":1},{"a":1,"b":2,"c":3}],"unspecified":{"a":1},"specified":{"a":1,"b":2,"c":3}}`, schema: &structuralschema.Structural{
|
||||||
Properties: map[string]structuralschema.Structural{
|
Properties: map[string]structuralschema.Structural{
|
||||||
"array": {
|
"array": {
|
||||||
Items: &structuralschema.Structural{
|
Items: &structuralschema.Structural{
|
||||||
@ -64,8 +66,8 @@ func TestPrune(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, `{"array":[{"a":1},{},{"a":1,"c":3}],"specified":{"a":1,"c":3}}`},
|
}, expected: `{"array":[{"a":1},{},{"a":1,"c":3}],"specified":{"a":1,"c":3}}`},
|
||||||
{"nested x-kubernetes-preserve-unknown-fields", `
|
{name: "nested x-kubernetes-preserve-unknown-fields", json: `
|
||||||
{
|
{
|
||||||
"unspecified":"bar",
|
"unspecified":"bar",
|
||||||
"alpha": "abc",
|
"alpha": "abc",
|
||||||
@ -84,7 +86,7 @@ func TestPrune(t *testing.T) {
|
|||||||
"preserving": {"unspecified": "bar"}
|
"preserving": {"unspecified": "bar"}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`, &structuralschema.Structural{
|
`, schema: &structuralschema.Structural{
|
||||||
Generic: structuralschema.Generic{Type: "object"},
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
|
Extensions: structuralschema.Extensions{XPreserveUnknownFields: true},
|
||||||
Properties: map[string]structuralschema.Structural{
|
Properties: map[string]structuralschema.Structural{
|
||||||
@ -116,7 +118,7 @@ func TestPrune(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, `
|
}, expected: `
|
||||||
{
|
{
|
||||||
"unspecified":"bar",
|
"unspecified":"bar",
|
||||||
"alpha": "abc",
|
"alpha": "abc",
|
||||||
@ -134,7 +136,7 @@ func TestPrune(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`},
|
`},
|
||||||
{"additionalProperties with schema", `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, &structuralschema.Structural{
|
{name: "additionalProperties with schema", json: `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, schema: &structuralschema.Structural{
|
||||||
Properties: map[string]structuralschema.Structural{
|
Properties: map[string]structuralschema.Structural{
|
||||||
"a": {},
|
"a": {},
|
||||||
"c": {
|
"c": {
|
||||||
@ -149,8 +151,8 @@ func TestPrune(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, `{"a":1,"c":{"a":1,"b":2,"c":{}}}`},
|
}, expected: `{"a":1,"c":{"a":1,"b":2,"c":{}}}`},
|
||||||
{"additionalProperties with bool", `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, &structuralschema.Structural{
|
{name: "additionalProperties with bool", json: `{"a":1,"b":1,"c":{"a":1,"b":2,"c":{"a":1}}}`, schema: &structuralschema.Structural{
|
||||||
Properties: map[string]structuralschema.Structural{
|
Properties: map[string]structuralschema.Structural{
|
||||||
"a": {},
|
"a": {},
|
||||||
"c": {
|
"c": {
|
||||||
@ -161,7 +163,313 @@ func TestPrune(t *testing.T) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, `{"a":1,"c":{"a":1,"b":2,"c":{}}}`},
|
}, expected: `{"a":1,"c":{"a":1,"b":2,"c":{}}}`},
|
||||||
|
{name: "x-kubernetes-embedded-resource", json: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"unspecified":"bar",
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar",
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, schema: &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"pruned": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"embedded": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, expected: `
|
||||||
|
{
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`},
|
||||||
|
{name: "x-kubernetes-embedded-resource, with root=true", json: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"unspecified":"bar",
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar",
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`, dontPruneMetaAtRoot: true, schema: &structuralschema.Structural{
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"pruned": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
XPreserveUnknownFields: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"embedded": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
Extensions: structuralschema.Extensions{
|
||||||
|
XEmbeddedResource: true,
|
||||||
|
},
|
||||||
|
Properties: map[string]structuralschema.Structural{
|
||||||
|
"spec": {
|
||||||
|
Generic: structuralschema.Generic{Type: "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}, expected: `
|
||||||
|
{
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"pruned": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"preserving": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"unspecified": "bar",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"unspecified": "bar"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nested": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
"embedded": {
|
||||||
|
"apiVersion": "foo/v1",
|
||||||
|
"kind": "Foo",
|
||||||
|
"metadata": {
|
||||||
|
"name": "instance",
|
||||||
|
"unspecified": "bar"
|
||||||
|
},
|
||||||
|
"spec": {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
@ -175,7 +483,7 @@ func TestPrune(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
prune(in, tt.schema)
|
Prune(in, tt.schema, tt.dontPruneMetaAtRoot)
|
||||||
if !reflect.DeepEqual(in, expected) {
|
if !reflect.DeepEqual(in, expected) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
enc := json.NewEncoder(&buf)
|
enc := json.NewEncoder(&buf)
|
||||||
@ -184,7 +492,7 @@ func TestPrune(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected result mashalling error: %v", err)
|
t.Fatalf("unexpected result mashalling error: %v", err)
|
||||||
}
|
}
|
||||||
t.Errorf("expected: %s\ngot: %s", tt.expected, buf.String())
|
t.Errorf("expected: %s\ngot: %s\ndiff: %s", tt.expected, buf.String(), diff.ObjectDiff(expected, in))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -260,7 +568,7 @@ func BenchmarkPrune(b *testing.B) {
|
|||||||
|
|
||||||
b.StartTimer()
|
b.StartTimer()
|
||||||
for i := 0; i < b.N; i++ {
|
for i := 0; i < b.N; i++ {
|
||||||
Prune(instances[i], schema)
|
Prune(instances[i], schema, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user