Skip to content
This repository was archived by the owner on Nov 16, 2022. It is now read-only.

Commit 77d1261

Browse files
author
Jonathan Vila
authored
KEYCLOAK-19442 ssl db access
1 parent bc0ec96 commit 77d1261

15 files changed

+520
-77
lines changed

pkg/common/cluster_state.go

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ type ClusterState struct {
5252
KeycloakPrometheusRule *monitoringv1.PrometheusRule
5353
KeycloakGrafanaDashboard *grafanav1alpha1.GrafanaDashboard
5454
DatabaseSecret *v1.Secret
55+
DatabaseSSLCert *v1.Secret
5556
PostgresqlPersistentVolumeClaim *v1.PersistentVolumeClaim
5657
PostgresqlService *v1.Service
5758
PostgresqlDeployment *v12.Deployment
@@ -98,6 +99,11 @@ func (i *ClusterState) Read(context context.Context, cr *kc.Keycloak, controller
9899
return err
99100
}
100101

102+
err = i.readDatabaseSSLSecretCurrentState(context, cr, controllerClient)
103+
if err != nil {
104+
return err
105+
}
106+
101107
err = i.readProbesCurrentState(context, cr, controllerClient)
102108
if err != nil {
103109
return err
@@ -351,6 +357,29 @@ func (i *ClusterState) readKeycloakGrafanaDashboardCurrentState(context context.
351357
return nil
352358
}
353359

360+
func (i *ClusterState) readDatabaseSSLSecretCurrentState(context context.Context, cr *kc.Keycloak, controllerClient client.Client) error {
361+
databaseSSLSecret := &v1.Secret{}
362+
databaseSSLSecretSelector := client.ObjectKey{
363+
Name: model.DatabaseSecretSslCert,
364+
Namespace: cr.Namespace,
365+
}
366+
367+
err := controllerClient.Get(context, databaseSSLSecretSelector, databaseSSLSecret)
368+
369+
if err != nil {
370+
// If the resource type doesn't exist on the cluster or does exist but is not found
371+
if meta.IsNoMatchError(err) || apiErrors.IsNotFound(err) {
372+
i.DatabaseSSLCert = nil
373+
} else {
374+
return err
375+
}
376+
} else {
377+
i.DatabaseSSLCert = databaseSSLSecret.DeepCopy()
378+
cr.UpdateStatusSecondaryResources(i.DatabaseSSLCert.Kind, i.DatabaseSSLCert.Name)
379+
}
380+
return nil
381+
}
382+
354383
func (i *ClusterState) readDatabaseSecretCurrentState(context context.Context, cr *kc.Keycloak, controllerClient client.Client) error {
355384
databaseSecret := model.DatabaseSecret(cr)
356385
databaseSecretSelector := model.DatabaseSecretSelector(cr)
@@ -394,10 +423,10 @@ func (i *ClusterState) readProbesCurrentState(context context.Context, cr *kc.Ke
394423
func (i *ClusterState) readKeycloakOrRHSSODeploymentCurrentState(context context.Context, cr *kc.Keycloak, controllerClient client.Client) error {
395424
isRHSSO := model.Profiles.IsRHSSO(cr)
396425

397-
deployment := model.KeycloakDeployment(cr, nil)
426+
deployment := model.KeycloakDeployment(cr, nil, nil)
398427
selector := model.KeycloakDeploymentSelector(cr)
399428
if isRHSSO {
400-
deployment = model.RHSSODeployment(cr, nil)
429+
deployment = model.RHSSODeployment(cr, nil, nil)
401430
selector = model.RHSSODeploymentSelector(cr)
402431
}
403432

pkg/controller/keycloak/keycloak_migrations_test.go

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ func TestKeycloakMigration_Test_No_Need_For_Migration_On_Missing_Deployment_In_D
3131
cr := &v1alpha1.Keycloak{}
3232
migrator, _ := GetMigrator(cr)
3333

34-
keycloakDeployment := model.KeycloakDeployment(cr, nil)
34+
keycloakDeployment := model.KeycloakDeployment(cr, nil, nil)
3535
SetDeployment(keycloakDeployment, 5, "old_image")
3636

3737
currentState := common.ClusterState{
@@ -56,10 +56,10 @@ func TestKeycloakMigration_Test_Migrating_Image(t *testing.T) {
5656
cr := &v1alpha1.Keycloak{}
5757
migrator, _ := GetMigrator(cr)
5858

59-
keycloakCurrentDeployment := model.KeycloakDeployment(cr, model.DatabaseSecret(cr))
59+
keycloakCurrentDeployment := model.KeycloakDeployment(cr, model.DatabaseSecret(cr), nil)
6060
SetDeployment(keycloakCurrentDeployment, 5, "old_image")
6161

62-
keycloakDesiredDeployment := model.KeycloakDeployment(cr, nil)
62+
keycloakDesiredDeployment := model.KeycloakDeployment(cr, nil, nil)
6363
SetDeployment(keycloakDesiredDeployment, 5, "")
6464

6565
currentState := common.ClusterState{
@@ -91,10 +91,10 @@ func TestKeycloakMigration_Test_Migrating_RHSSO_Image(t *testing.T) {
9191
}
9292
migrator, _ := GetMigrator(cr)
9393

94-
keycloakCurrentDeployment := model.RHSSODeployment(cr, model.DatabaseSecret(cr))
94+
keycloakCurrentDeployment := model.RHSSODeployment(cr, model.DatabaseSecret(cr), nil)
9595
SetDeployment(keycloakCurrentDeployment, 5, "old_image")
9696

97-
keycloakDesiredDeployment := model.RHSSODeployment(cr, model.DatabaseSecret(cr))
97+
keycloakDesiredDeployment := model.RHSSODeployment(cr, model.DatabaseSecret(cr), nil)
9898
SetDeployment(keycloakDesiredDeployment, 5, "")
9999

100100
currentState := common.ClusterState{
@@ -131,10 +131,10 @@ func TBackup(t *testing.T, backupEnabled bool) {
131131
cr.Spec.Migration.Backups.Enabled = backupEnabled
132132
migrator, _ := GetMigrator(cr)
133133

134-
keycloakCurrentDeployment := model.KeycloakDeployment(cr, nil)
134+
keycloakCurrentDeployment := model.KeycloakDeployment(cr, nil, nil)
135135
SetDeployment(keycloakCurrentDeployment, 0, "old_image")
136136

137-
keycloakDesiredDeployment := model.KeycloakDeployment(cr, nil)
137+
keycloakDesiredDeployment := model.KeycloakDeployment(cr, nil, nil)
138138
SetDeployment(keycloakDesiredDeployment, 0, "")
139139

140140
currentState := common.ClusterState{
@@ -164,10 +164,10 @@ func TestKeycloakMigration_Test_No_Migration_Happens_With_Rolling_Migrator(t *te
164164
cr.Spec.Migration.MigrationStrategy = v1alpha1.StrategyRolling
165165
migrator, _ := GetMigrator(cr)
166166

167-
keycloakCurrentDeployment := model.RHSSODeployment(cr, model.DatabaseSecret(cr))
167+
keycloakCurrentDeployment := model.RHSSODeployment(cr, model.DatabaseSecret(cr), nil)
168168
SetDeployment(keycloakCurrentDeployment, 5, "old_image")
169169

170-
keycloakDesiredDeployment := model.RHSSODeployment(cr, model.DatabaseSecret(cr))
170+
keycloakDesiredDeployment := model.RHSSODeployment(cr, model.DatabaseSecret(cr), nil)
171171
SetDeployment(keycloakDesiredDeployment, 5, "")
172172

173173
currentState := common.ClusterState{

pkg/controller/keycloak/keycloak_reconciler.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,11 @@ func (i *KeycloakReconciler) getDatabaseSecretDesiredState(clusterState *common.
298298
func (i *KeycloakReconciler) getKeycloakDeploymentOrRHSSODesiredState(clusterState *common.ClusterState, cr *kc.Keycloak) common.ClusterAction {
299299
isRHSSO := model.Profiles.IsRHSSO(cr)
300300

301-
deployment := model.KeycloakDeployment(cr, clusterState.DatabaseSecret)
301+
deployment := model.KeycloakDeployment(cr, clusterState.DatabaseSecret, clusterState.DatabaseSSLCert)
302302
deploymentName := "Keycloak"
303303

304304
if isRHSSO {
305-
deployment = model.RHSSODeployment(cr, clusterState.DatabaseSecret)
305+
deployment = model.RHSSODeployment(cr, clusterState.DatabaseSecret, clusterState.DatabaseSSLCert)
306306
deploymentName = model.RHSSOProfile
307307
}
308308

@@ -313,9 +313,9 @@ func (i *KeycloakReconciler) getKeycloakDeploymentOrRHSSODesiredState(clusterSta
313313
}
314314
}
315315

316-
deploymentReconciled := model.KeycloakDeploymentReconciled(cr, clusterState.KeycloakDeployment, clusterState.DatabaseSecret)
316+
deploymentReconciled := model.KeycloakDeploymentReconciled(cr, clusterState.KeycloakDeployment, clusterState.DatabaseSecret, clusterState.DatabaseSSLCert)
317317
if isRHSSO {
318-
deploymentReconciled = model.RHSSODeploymentReconciled(cr, clusterState.KeycloakDeployment, clusterState.DatabaseSecret)
318+
deploymentReconciled = model.RHSSODeploymentReconciled(cr, clusterState.KeycloakDeployment, clusterState.DatabaseSecret, clusterState.DatabaseSSLCert)
319319
}
320320

321321
return common.GenericUpdateAction{

pkg/controller/keycloak/keycloak_reconciler_test.go

Lines changed: 115 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package keycloak
33
import (
44
"reflect"
55
"strconv"
6+
"strings"
67
"testing"
78

89
"k8s.io/apimachinery/pkg/api/resource"
@@ -86,7 +87,7 @@ func TestKeycloakReconciler_Test_Creating_All(t *testing.T) {
8687
assert.IsType(t, model.KeycloakDiscoveryService(cr), desiredState[9].(common.GenericCreateAction).Ref)
8788
assert.IsType(t, model.KeycloakMonitoringService(cr), desiredState[10].(common.GenericCreateAction).Ref)
8889
assert.IsType(t, model.KeycloakProbes(cr), desiredState[11].(common.GenericCreateAction).Ref)
89-
assert.IsType(t, model.KeycloakDeployment(cr, model.DatabaseSecret(cr)), desiredState[12].(common.GenericCreateAction).Ref)
90+
assert.IsType(t, model.KeycloakDeployment(cr, model.DatabaseSecret(cr), nil), desiredState[12].(common.GenericCreateAction).Ref)
9091
assert.IsType(t, model.KeycloakRoute(cr), desiredState[13].(common.GenericCreateAction).Ref)
9192
}
9293

@@ -114,7 +115,7 @@ func TestKeycloakReconciler_Test_Creating_RHSSO(t *testing.T) {
114115
if reflect.TypeOf(v) != reflect.TypeOf(common.GenericCreateAction{}) {
115116
allCreateActions = false
116117
}
117-
if reflect.TypeOf(v.(common.GenericCreateAction).Ref) == reflect.TypeOf(model.RHSSODeployment(cr, model.DatabaseSecret(cr))) {
118+
if reflect.TypeOf(v.(common.GenericCreateAction).Ref) == reflect.TypeOf(model.RHSSODeployment(cr, model.DatabaseSecret(cr), nil)) {
118119
deployment = v.(common.GenericCreateAction).Ref.(*v13.StatefulSet)
119120
}
120121
if reflect.TypeOf(v.(common.GenericCreateAction).Ref) == reflect.TypeOf(model.KeycloakIngress(cr)) {
@@ -124,7 +125,7 @@ func TestKeycloakReconciler_Test_Creating_RHSSO(t *testing.T) {
124125
assert.True(t, allCreateActions)
125126
assert.NotNil(t, deployment)
126127
assert.NotNil(t, ingress)
127-
assert.Equal(t, model.RHSSODeployment(cr, nil), deployment)
128+
assert.Equal(t, model.RHSSODeployment(cr, nil, nil), deployment)
128129
}
129130

130131
func TestKeycloakReconciler_Test_Updating_RHSSO(t *testing.T) {
@@ -148,7 +149,7 @@ func TestKeycloakReconciler_Test_Updating_RHSSO(t *testing.T) {
148149
PostgresqlDeployment: model.PostgresqlDeployment(cr, true),
149150
KeycloakService: model.KeycloakService(cr),
150151
KeycloakDiscoveryService: model.KeycloakDiscoveryService(cr),
151-
KeycloakDeployment: model.RHSSODeployment(cr, model.DatabaseSecret(cr)),
152+
KeycloakDeployment: model.RHSSODeployment(cr, model.DatabaseSecret(cr), nil),
152153
KeycloakAdminSecret: model.KeycloakAdminSecret(cr),
153154
KeycloakIngress: model.KeycloakIngress(cr),
154155
KeycloakProbes: model.KeycloakProbes(cr),
@@ -165,13 +166,13 @@ func TestKeycloakReconciler_Test_Updating_RHSSO(t *testing.T) {
165166
if reflect.TypeOf(v) != reflect.TypeOf(common.GenericUpdateAction{}) {
166167
allUpdateActions = false
167168
}
168-
if reflect.TypeOf(v.(common.GenericUpdateAction).Ref) == reflect.TypeOf(model.RHSSODeployment(cr, model.DatabaseSecret(cr))) {
169+
if reflect.TypeOf(v.(common.GenericUpdateAction).Ref) == reflect.TypeOf(model.RHSSODeployment(cr, model.DatabaseSecret(cr), nil)) {
169170
deployment = v.(common.GenericUpdateAction).Ref.(*v13.StatefulSet)
170171
}
171172
}
172173
assert.True(t, allUpdateActions)
173174
assert.NotNil(t, deployment)
174-
assert.Equal(t, model.RHSSODeployment(cr, model.DatabaseSecret(cr)), deployment)
175+
assert.Equal(t, model.RHSSODeployment(cr, model.DatabaseSecret(cr), nil), deployment)
175176
}
176177

177178
func TestKeycloakReconciler_Test_Updating_All(t *testing.T) {
@@ -192,7 +193,7 @@ func TestKeycloakReconciler_Test_Updating_All(t *testing.T) {
192193
KeycloakService: model.KeycloakService(cr),
193194
KeycloakDiscoveryService: model.KeycloakDiscoveryService(cr),
194195
KeycloakMonitoringService: model.KeycloakMonitoringService(cr),
195-
KeycloakDeployment: model.KeycloakDeployment(cr, model.DatabaseSecret(cr)),
196+
KeycloakDeployment: model.KeycloakDeployment(cr, model.DatabaseSecret(cr), nil),
196197
KeycloakAdminSecret: model.KeycloakAdminSecret(cr),
197198
KeycloakRoute: model.KeycloakRoute(cr),
198199
KeycloakMetricsRoute: model.KeycloakMetricsRoute(cr, model.KeycloakRoute(cr)),
@@ -253,7 +254,7 @@ func TestKeycloakReconciler_Test_Updating_All(t *testing.T) {
253254
assert.IsType(t, model.KeycloakService(cr), desiredState[8].(common.GenericUpdateAction).Ref)
254255
assert.IsType(t, model.KeycloakDiscoveryService(cr), desiredState[9].(common.GenericUpdateAction).Ref)
255256
assert.IsType(t, model.KeycloakMonitoringService(cr), desiredState[10].(common.GenericUpdateAction).Ref)
256-
assert.IsType(t, model.KeycloakDeployment(cr, model.DatabaseSecret(cr)), desiredState[11].(common.GenericUpdateAction).Ref)
257+
assert.IsType(t, model.KeycloakDeployment(cr, model.DatabaseSecret(cr), nil), desiredState[11].(common.GenericUpdateAction).Ref)
257258
assert.IsType(t, model.KeycloakMetricsRoute(cr, model.KeycloakRoute(cr)), desiredState[12].(common.GenericUpdateAction).Ref)
258259
}
259260

@@ -306,7 +307,7 @@ func TestKeycloakReconciler_Test_Creating_All_With_External_Database(t *testing.
306307
assert.IsType(t, model.KeycloakService(cr), desiredState[2].(common.GenericCreateAction).Ref)
307308
assert.IsType(t, model.KeycloakDiscoveryService(cr), desiredState[3].(common.GenericCreateAction).Ref)
308309
assert.IsType(t, model.KeycloakProbes(cr), desiredState[4].(common.GenericCreateAction).Ref)
309-
assert.IsType(t, model.KeycloakDeployment(cr, model.DatabaseSecret(cr)), desiredState[5].(common.GenericCreateAction).Ref)
310+
assert.IsType(t, model.KeycloakDeployment(cr, model.DatabaseSecret(cr), nil), desiredState[5].(common.GenericCreateAction).Ref)
310311
}
311312

312313
func TestKeycloakReconciler_Test_Updating_External_Database_WithIPAddress(t *testing.T) {
@@ -382,6 +383,109 @@ func TestKeycloakReconciler_Test_Updating_External_Database_URI(t *testing.T) {
382383
assert.Nil(t, service.Spec.Selector)
383384
}
384385

386+
func TestKeycloakReconciler_Test_Given_SSLMODE_When_Reconcile_Then_NewEnvVarsAndMountedVolume(t *testing.T) {
387+
// given
388+
cr := &v1alpha1.Keycloak{}
389+
cr.Spec.ExternalDatabase.Enabled = true
390+
391+
currentState := common.NewClusterState()
392+
currentState.DatabaseSecret = model.DatabaseSecret(cr)
393+
currentState.DatabaseSecret.Data[model.DatabaseSecretSslModeProperty] = []byte("required")
394+
currentState.DatabaseSSLCert = model.DatabaseSecret(cr)
395+
396+
// when
397+
reconciler := NewKeycloakReconciler()
398+
desiredState := reconciler.Reconcile(currentState, cr)
399+
// element 5 is the KeycloakDeployment
400+
keycloakSpec := desiredState[5].(common.GenericCreateAction).Ref.(*v13.StatefulSet).Spec.Template.Spec
401+
402+
// then
403+
envVarOk := false
404+
for _, a := range keycloakSpec.Containers[0].Env {
405+
if a.Name == model.KeycloakDatabaseConnectionParamsProperty && strings.Contains(a.Value, "sslmode=required") {
406+
envVarOk = true
407+
}
408+
}
409+
assert.True(t, envVarOk)
410+
411+
sslVolumeExists := false
412+
for _, volume := range keycloakSpec.Volumes {
413+
if strings.Contains(volume.Name, model.DatabaseSecretSslCert+"-vol") {
414+
sslVolumeExists = true
415+
}
416+
}
417+
assert.True(t, sslVolumeExists)
418+
}
419+
420+
func TestKeycloakReconciler_Test_Given_SSLMODE_And_RHSSO_When_Reconcile_Then_NewEnvVarsAndMountedVolume(t *testing.T) {
421+
// given
422+
cr := &v1alpha1.Keycloak{}
423+
cr.Spec.ExternalDatabase.Enabled = true
424+
cr.Spec.Profile = "RHSSO"
425+
426+
currentState := common.NewClusterState()
427+
currentState.DatabaseSecret = model.DatabaseSecret(cr)
428+
currentState.DatabaseSecret.Data[model.DatabaseSecretSslModeProperty] = []byte("required")
429+
currentState.DatabaseSSLCert = model.DatabaseSecret(cr)
430+
431+
// when
432+
reconciler := NewKeycloakReconciler()
433+
desiredState := reconciler.Reconcile(currentState, cr)
434+
// element 5 is the KeycloakDeployment
435+
keycloakSpec := desiredState[5].(common.GenericCreateAction).Ref.(*v13.StatefulSet).Spec.Template.Spec
436+
437+
// then
438+
envVarFound := 0
439+
for _, a := range keycloakSpec.Containers[0].Env {
440+
if strings.Contains(a.Name, model.RhssoDatabaseNONXAConnectionParamsProperty) && strings.EqualFold(a.Value, "required") {
441+
envVarFound++
442+
} else if strings.Contains(a.Name, model.RhssoDatabaseXAConnectionParamsProperty) && strings.EqualFold(a.Value, "required") {
443+
envVarFound++
444+
}
445+
}
446+
assert.Equal(t, 2, envVarFound)
447+
448+
sslVolumeExists := false
449+
for _, volume := range keycloakSpec.Volumes {
450+
if strings.Contains(volume.Name, model.DatabaseSecretSslCert+"-vol") {
451+
sslVolumeExists = true
452+
}
453+
}
454+
assert.True(t, sslVolumeExists)
455+
}
456+
457+
func TestKeycloakReconciler_Test_Given_NoSSLMODE_When_Reconcile_Then_NoNewEnvVarsAndMountedVolume(t *testing.T) {
458+
// given
459+
cr := &v1alpha1.Keycloak{}
460+
cr.Spec.ExternalDatabase.Enabled = true
461+
462+
currentState := common.NewClusterState()
463+
currentState.DatabaseSecret = model.DatabaseSecret(cr)
464+
465+
// when
466+
reconciler := NewKeycloakReconciler()
467+
desiredState := reconciler.Reconcile(currentState, cr)
468+
// element 5 is the KeycloakDeployment
469+
keycloakSpec := desiredState[5].(common.GenericCreateAction).Ref.(*v13.StatefulSet).Spec.Template.Spec
470+
471+
// then
472+
sslVolumeExists := false
473+
for _, volume := range keycloakSpec.Volumes {
474+
if strings.Contains(volume.Name, model.DatabaseSecretSslCert+"-vol") {
475+
sslVolumeExists = true
476+
}
477+
}
478+
assert.False(t, sslVolumeExists)
479+
480+
envVarOk := false
481+
for _, a := range keycloakSpec.Containers[0].Env {
482+
if a.Name == model.KeycloakDatabaseConnectionParamsProperty && strings.Contains(a.Value, "sslmode") {
483+
envVarOk = true
484+
}
485+
}
486+
assert.False(t, envVarOk)
487+
}
488+
385489
func TestKeycloakReconciler_Test_Updating_External_Database_URI_From_IP_To_ExternalName(t *testing.T) {
386490
// given
387491
const (
@@ -612,7 +716,7 @@ func TestKeycloakReconciler_Test_Setting_Resources(t *testing.T) {
612716
// 12) Keycloak StatefulSets
613717
assert.Equal(t, 14, len(desiredState))
614718
assert.IsType(t, model.PostgresqlDeployment(cr, false), desiredState[6].(common.GenericCreateAction).Ref)
615-
assert.IsType(t, model.KeycloakDeployment(cr, model.DatabaseSecret(cr)), desiredState[12].(common.GenericCreateAction).Ref)
719+
assert.IsType(t, model.KeycloakDeployment(cr, model.DatabaseSecret(cr), nil), desiredState[12].(common.GenericCreateAction).Ref)
616720
keycloakContainer := desiredState[12].(common.GenericCreateAction).Ref.(*v13.StatefulSet).Spec.Template.Spec.Containers[0]
617721
assert.Equal(t, &resource700Mi, keycloakContainer.Resources.Requests.Memory(), "Keycloak Deployment: Memory-Requests should be: "+resource700Mi.String()+" but is "+keycloakContainer.Resources.Requests.Memory().String())
618722
assert.Equal(t, &resource1900m, keycloakContainer.Resources.Requests.Cpu(), "Keycloak Deployment: Cpu-Requests should be: "+resource1900m.String()+" but is "+keycloakContainer.Resources.Requests.Cpu().String())
@@ -651,7 +755,7 @@ func TestKeycloakReconciler_Test_No_Resources_Specified(t *testing.T) {
651755
// 12) Keycloak StatefulSets
652756
assert.Equal(t, 14, len(desiredState))
653757
assert.IsType(t, model.PostgresqlDeployment(cr, true), desiredState[6].(common.GenericCreateAction).Ref)
654-
assert.IsType(t, model.KeycloakDeployment(cr, model.DatabaseSecret(cr)), desiredState[12].(common.GenericCreateAction).Ref)
758+
assert.IsType(t, model.KeycloakDeployment(cr, model.DatabaseSecret(cr), nil), desiredState[12].(common.GenericCreateAction).Ref)
655759
keycloakContainer := desiredState[12].(common.GenericCreateAction).Ref.(*v13.StatefulSet).Spec.Template.Spec.Containers[0]
656760
assert.Equal(t, 0, len(keycloakContainer.Resources.Requests), "Requests-List should be empty")
657761
assert.Equal(t, 0, len(keycloakContainer.Resources.Limits), "Limits-List should be empty")

0 commit comments

Comments
 (0)