Merge pull request #5742 from anujagrawal699/addedTests-pkg/util/helper/work.go
Added tests for pkg/util/helper/work.go
This commit is contained in:
commit
492e24dd8c
|
@ -17,13 +17,360 @@ limitations under the License.
|
||||||
package helper
|
package helper
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"reflect"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
|
"sigs.k8s.io/controller-runtime/pkg/client/fake"
|
||||||
|
|
||||||
|
workv1alpha1 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha1"
|
||||||
|
workv1alpha2 "github.com/karmada-io/karmada/pkg/apis/work/v1alpha2"
|
||||||
|
"github.com/karmada-io/karmada/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestCreateOrUpdateWork(t *testing.T) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
assert.NoError(t, workv1alpha1.Install(scheme))
|
||||||
|
assert.NoError(t, workv1alpha2.Install(scheme))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
existingWork *workv1alpha1.Work
|
||||||
|
workMeta metav1.ObjectMeta
|
||||||
|
resource *unstructured.Unstructured
|
||||||
|
wantErr bool
|
||||||
|
verify func(*testing.T, client.Client)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "create new work",
|
||||||
|
workMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-work",
|
||||||
|
},
|
||||||
|
resource: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "apps/v1",
|
||||||
|
"kind": "Deployment",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "test-deployment",
|
||||||
|
"uid": "test-uid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verify: func(t *testing.T, c client.Client) {
|
||||||
|
work := &workv1alpha1.Work{}
|
||||||
|
err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, "test-work", work.Name)
|
||||||
|
assert.Equal(t, 1, len(work.Spec.Workload.Manifests))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create work with PropagationInstruction",
|
||||||
|
workMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-work",
|
||||||
|
Labels: map[string]string{
|
||||||
|
util.PropagationInstruction: "some-value",
|
||||||
|
},
|
||||||
|
Annotations: map[string]string{
|
||||||
|
workv1alpha2.ResourceConflictResolutionAnnotation: "overwrite",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resource: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "apps/v1",
|
||||||
|
"kind": "Deployment",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "test-deployment",
|
||||||
|
"uid": "test-uid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verify: func(t *testing.T, c client.Client) {
|
||||||
|
work := &workv1alpha1.Work{}
|
||||||
|
err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Get the resource from manifests
|
||||||
|
manifest := &unstructured.Unstructured{}
|
||||||
|
err = manifest.UnmarshalJSON(work.Spec.Workload.Manifests[0].Raw)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify labels and annotations were set
|
||||||
|
labels := manifest.GetLabels()
|
||||||
|
assert.Equal(t, util.ManagedByKarmadaLabelValue, labels[util.ManagedByKarmadaLabel])
|
||||||
|
|
||||||
|
annotations := manifest.GetAnnotations()
|
||||||
|
assert.Equal(t, "test-uid", annotations[workv1alpha2.ResourceTemplateUIDAnnotation])
|
||||||
|
assert.Equal(t, "test-work", annotations[workv1alpha2.WorkNameAnnotation])
|
||||||
|
assert.Equal(t, "default", annotations[workv1alpha2.WorkNamespaceAnnotation])
|
||||||
|
assert.Equal(t, "overwrite", annotations[workv1alpha2.ResourceConflictResolutionAnnotation])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create work with PropagationInstructionSuppressed",
|
||||||
|
workMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-work",
|
||||||
|
Labels: map[string]string{
|
||||||
|
util.PropagationInstruction: util.PropagationInstructionSuppressed,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
resource: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "apps/v1",
|
||||||
|
"kind": "Deployment",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "test-deployment",
|
||||||
|
"uid": "test-uid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verify: func(t *testing.T, c client.Client) {
|
||||||
|
work := &workv1alpha1.Work{}
|
||||||
|
err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Get the resource from manifests
|
||||||
|
manifest := &unstructured.Unstructured{}
|
||||||
|
err = manifest.UnmarshalJSON(work.Spec.Workload.Manifests[0].Raw)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify labels and annotations were NOT set
|
||||||
|
labels := manifest.GetLabels()
|
||||||
|
assert.Empty(t, labels[util.ManagedByKarmadaLabel])
|
||||||
|
|
||||||
|
annotations := manifest.GetAnnotations()
|
||||||
|
assert.Empty(t, annotations[workv1alpha2.ResourceTemplateUIDAnnotation])
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update existing work",
|
||||||
|
existingWork: &workv1alpha1.Work{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-work",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-work",
|
||||||
|
},
|
||||||
|
resource: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "apps/v1",
|
||||||
|
"kind": "Deployment",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "test-deployment",
|
||||||
|
"uid": "test-uid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
verify: func(t *testing.T, c client.Client) {
|
||||||
|
work := &workv1alpha1.Work{}
|
||||||
|
err := c.Get(context.TODO(), client.ObjectKey{Namespace: "default", Name: "test-work"}, work)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, 1, len(work.Spec.Workload.Manifests))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "error when work is being deleted",
|
||||||
|
existingWork: &workv1alpha1.Work{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-work",
|
||||||
|
DeletionTimestamp: &metav1.Time{Time: time.Now()},
|
||||||
|
Finalizers: []string{"test.finalizer.io"}, // Finalizer to satisfy fake client requirement
|
||||||
|
},
|
||||||
|
Spec: workv1alpha1.WorkSpec{
|
||||||
|
Workload: workv1alpha1.WorkloadTemplate{
|
||||||
|
Manifests: []workv1alpha1.Manifest{
|
||||||
|
{
|
||||||
|
RawExtension: runtime.RawExtension{
|
||||||
|
Raw: []byte(`{"apiVersion":"apps/v1","kind":"Deployment","metadata":{"name":"test-deployment"}}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
workMeta: metav1.ObjectMeta{
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "test-work",
|
||||||
|
},
|
||||||
|
resource: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "apps/v1",
|
||||||
|
"kind": "Deployment",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "test-deployment",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
c := fake.NewClientBuilder().WithScheme(scheme)
|
||||||
|
if tt.existingWork != nil {
|
||||||
|
c = c.WithObjects(tt.existingWork)
|
||||||
|
}
|
||||||
|
client := c.Build()
|
||||||
|
|
||||||
|
err := CreateOrUpdateWork(context.TODO(), client, tt.workMeta, tt.resource)
|
||||||
|
|
||||||
|
if tt.wantErr {
|
||||||
|
assert.Error(t, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
assert.NoError(t, err)
|
||||||
|
if tt.verify != nil {
|
||||||
|
tt.verify(t, client)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetWorksByLabelsSet(t *testing.T) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
assert.NoError(t, workv1alpha1.Install(scheme))
|
||||||
|
|
||||||
|
work1 := &workv1alpha1.Work{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "work1",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"app": "test",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
work2 := &workv1alpha1.Work{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "work2",
|
||||||
|
Labels: map[string]string{
|
||||||
|
"app": "other",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
works []client.Object
|
||||||
|
labels labels.Set
|
||||||
|
wantCount int
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no works exist",
|
||||||
|
works: []client.Object{},
|
||||||
|
labels: labels.Set{"app": "test"},
|
||||||
|
wantCount: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find work by label",
|
||||||
|
works: []client.Object{work1, work2},
|
||||||
|
labels: labels.Set{"app": "test"},
|
||||||
|
wantCount: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find multiple works",
|
||||||
|
works: []client.Object{work1, work2},
|
||||||
|
labels: labels.Set{},
|
||||||
|
wantCount: 2,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
WithObjects(tt.works...).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
workList, err := GetWorksByLabelsSet(context.TODO(), client, tt.labels)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.wantCount, len(workList.Items))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetWorksByBindingID(t *testing.T) {
|
||||||
|
scheme := runtime.NewScheme()
|
||||||
|
assert.NoError(t, workv1alpha1.Install(scheme))
|
||||||
|
|
||||||
|
bindingID := "test-binding-id"
|
||||||
|
work1 := &workv1alpha1.Work{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "work1",
|
||||||
|
Labels: map[string]string{
|
||||||
|
workv1alpha2.ResourceBindingPermanentIDLabel: bindingID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
work2 := &workv1alpha1.Work{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "work2",
|
||||||
|
Labels: map[string]string{
|
||||||
|
workv1alpha2.ClusterResourceBindingPermanentIDLabel: bindingID,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
works []client.Object
|
||||||
|
bindingID string
|
||||||
|
namespaced bool
|
||||||
|
wantWorks []string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "find namespaced binding works",
|
||||||
|
works: []client.Object{work1, work2},
|
||||||
|
bindingID: bindingID,
|
||||||
|
namespaced: true,
|
||||||
|
wantWorks: []string{"work1"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find cluster binding works",
|
||||||
|
works: []client.Object{work1, work2},
|
||||||
|
bindingID: bindingID,
|
||||||
|
namespaced: false,
|
||||||
|
wantWorks: []string{"work2"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
client := fake.NewClientBuilder().
|
||||||
|
WithScheme(scheme).
|
||||||
|
WithObjects(tt.works...).
|
||||||
|
Build()
|
||||||
|
|
||||||
|
workList, err := GetWorksByBindingID(context.TODO(), client, tt.bindingID, tt.namespaced)
|
||||||
|
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Equal(t, len(tt.wantWorks), len(workList.Items))
|
||||||
|
|
||||||
|
// Verify the correct works were returned
|
||||||
|
foundWorkNames := make([]string, len(workList.Items))
|
||||||
|
for i, work := range workList.Items {
|
||||||
|
foundWorkNames[i] = work.Name
|
||||||
|
}
|
||||||
|
assert.ElementsMatch(t, tt.wantWorks, foundWorkNames)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestGenEventRef(t *testing.T) {
|
func TestGenEventRef(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -93,17 +440,157 @@ func TestGenEventRef(t *testing.T) {
|
||||||
},
|
},
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "empty kind",
|
||||||
|
obj: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "test-obj",
|
||||||
|
"uid": "test-uid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty name",
|
||||||
|
obj: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Pod",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"uid": "test-uid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "missing uid but has annotation",
|
||||||
|
obj: &unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Pod",
|
||||||
|
"metadata": map[string]interface{}{
|
||||||
|
"name": "test-pod",
|
||||||
|
"annotations": map[string]interface{}{
|
||||||
|
workv1alpha2.ResourceTemplateUIDAnnotation: "annotation-uid",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: &corev1.ObjectReference{
|
||||||
|
APIVersion: "v1",
|
||||||
|
Kind: "Pod",
|
||||||
|
Name: "test-pod",
|
||||||
|
UID: "annotation-uid",
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
actual, err := GenEventRef(tt.obj)
|
actual, err := GenEventRef(tt.obj)
|
||||||
if (err != nil) != tt.wantErr {
|
if tt.wantErr {
|
||||||
t.Errorf("GenEventRef() error = %v, wantErr %v", err, tt.wantErr)
|
assert.Error(t, err)
|
||||||
return
|
assert.Nil(t, actual)
|
||||||
}
|
} else {
|
||||||
if !reflect.DeepEqual(actual, tt.want) {
|
assert.NoError(t, err)
|
||||||
t.Errorf("GenEventRef() = %v, want %v", actual, tt.want)
|
assert.Equal(t, tt.want, actual)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestIsWorkContains(t *testing.T) {
|
||||||
|
deployment := unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "apps/v1",
|
||||||
|
"kind": "Deployment",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
deploymentData, _ := deployment.MarshalJSON()
|
||||||
|
|
||||||
|
service := unstructured.Unstructured{
|
||||||
|
Object: map[string]interface{}{
|
||||||
|
"apiVersion": "v1",
|
||||||
|
"kind": "Service",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
serviceData, _ := service.MarshalJSON()
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
manifests []workv1alpha1.Manifest
|
||||||
|
targetResource schema.GroupVersionKind
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "resource exists in manifests",
|
||||||
|
manifests: []workv1alpha1.Manifest{
|
||||||
|
{RawExtension: runtime.RawExtension{Raw: deploymentData}},
|
||||||
|
{RawExtension: runtime.RawExtension{Raw: serviceData}},
|
||||||
|
},
|
||||||
|
targetResource: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resource does not exist in manifests",
|
||||||
|
manifests: []workv1alpha1.Manifest{
|
||||||
|
{RawExtension: runtime.RawExtension{Raw: serviceData}},
|
||||||
|
},
|
||||||
|
targetResource: schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := IsWorkContains(tt.manifests, tt.targetResource)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsWorkSuspendDispatching(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
work *workv1alpha1.Work
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "dispatching is suspended",
|
||||||
|
work: &workv1alpha1.Work{
|
||||||
|
Spec: workv1alpha1.WorkSpec{
|
||||||
|
SuspendDispatching: ptr.To(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "dispatching is not suspended",
|
||||||
|
work: &workv1alpha1.Work{
|
||||||
|
Spec: workv1alpha1.WorkSpec{
|
||||||
|
SuspendDispatching: ptr.To(false),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "suspend dispatching is nil",
|
||||||
|
work: &workv1alpha1.Work{
|
||||||
|
Spec: workv1alpha1.WorkSpec{},
|
||||||
|
},
|
||||||
|
want: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
got := IsWorkSuspendDispatching(tt.work)
|
||||||
|
assert.Equal(t, tt.want, got)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue