Skip to content

Commit 4bd7ad1

Browse files
marinakogrjeberhard
authored andcommitted
Update ItHPACustomMetrics and ItIstioMonitoringExporter tests to run on internal Jenkin
1 parent f6ac656 commit 4bd7ad1

File tree

7 files changed

+189
-41
lines changed

7 files changed

+189
-41
lines changed

Jenkinsfile.oke

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -651,7 +651,7 @@ EOF
651651
${WORKSPACE}/terraform/oke.delete.sh ${OCI_PROP_FILE} ${WORKSPACE}/terraform ${AVAILABILITY_DOMAIN}
652652
fi
653653

654-
if [ "${MAVEN_PROFILE_NAME}" = "oke-gate" ]; then
654+
if [ "${MAVEN_PROFILE_NAME}" = "oke-gate" ] && [ "${BRANCH}" = "main" ]; then
655655
compname="wkt"
656656
wkt_compartment_ocid=$(oci iam compartment list --compartment-id-in-subtree true --all | jq --arg compname "$compname" '.data[] | select(."name"==$compname)' | jq -r ."id")
657657
sec_list_id=$(oci network security-list list --compartment-id="$wkt_compartment_ocid" --display-name=Security-List-wktiso1 | jq -r '.data[] | ."id"')

integration-tests/src/test/java/oracle/weblogic/kubernetes/ItHorizontalPodAutoscalerCustomMetrics.java

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2022, 2023, Oracle and/or its affiliates.
1+
// Copyright (c) 2022, 2024, Oracle and/or its affiliates.
22
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
33

44
package oracle.weblogic.kubernetes;
@@ -50,6 +50,7 @@
5050
import static oracle.weblogic.kubernetes.TestConstants.K8S_NODEPORT_HOST;
5151
import static oracle.weblogic.kubernetes.TestConstants.KUBERNETES_CLI;
5252
import static oracle.weblogic.kubernetes.TestConstants.MANAGED_SERVER_NAME_BASE;
53+
import static oracle.weblogic.kubernetes.TestConstants.OKE_CLUSTER_PRIVATEIP;
5354
import static oracle.weblogic.kubernetes.TestConstants.PROMETHEUS_CHART_VERSION;
5455
import static oracle.weblogic.kubernetes.TestConstants.RESULTS_ROOT;
5556
import static oracle.weblogic.kubernetes.TestConstants.TEST_IMAGES_REPO_SECRET_NAME;
@@ -67,6 +68,9 @@
6768
import static oracle.weblogic.kubernetes.utils.CommonMiiTestUtils.createDomainResource;
6869
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.checkClusterReplicaCountMatches;
6970
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.checkPodReadyAndServiceExists;
71+
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.createIngressPathRouting;
72+
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.formatIPv6Host;
73+
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.getServiceExtIPAddrtOke;
7074
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.testUntil;
7175
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.withLongRetryPolicy;
7276
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.withStandardRetryPolicy;
@@ -98,7 +102,7 @@
98102
@DisplayName("Test to a create MII domain and test autoscaling using HPA and"
99103
+ "custom metrics provided via use of monitoring exporter and prometheus and prometheus adapter")
100104
@IntegrationTest
101-
@Tag("oke-sequential")
105+
@Tag("oke-gate")
102106
@Tag("kind-parallel")
103107
public class ItHorizontalPodAutoscalerCustomMetrics {
104108
private static final String MONEXP_MODEL_FILE = "model.monexp.custommetrics.yaml";
@@ -107,6 +111,7 @@ public class ItHorizontalPodAutoscalerCustomMetrics {
107111
private static final String SESSMIGR_APP_WAR_NAME = "sessmigr-war";
108112
private static final String SESSMIGT_APP_URL = SESSMIGR_APP_WAR_NAME + "/?getCounter";
109113
private static final String domainUid = "hpacustomdomain";
114+
private static String ingressIP = null;
110115

111116
private static String domainNamespace = null;
112117
private static String wlClusterName = "cluster-1";
@@ -213,6 +218,9 @@ public static void initAll(@Namespaces(4) List<String> namespaces) {
213218
logger.info("NGINX service name: {0}", nginxServiceName);
214219
nodeportshttp = getServiceNodePort(nginxNamespace, nginxServiceName, "http");
215220
logger.info("NGINX http node port: {0}", nodeportshttp);
221+
String host = formatIPv6Host(K8S_NODEPORT_HOST);
222+
ingressIP = getServiceExtIPAddrtOke(nginxServiceName, nginxNamespace) != null
223+
? getServiceExtIPAddrtOke(nginxServiceName, nginxNamespace) : host;
216224

217225
// create cluster resouce with limits and requests in serverPod
218226
ClusterResource clusterResource =
@@ -254,9 +262,12 @@ void testHPAWithCustomMetrics() {
254262
assertDoesNotThrow(() -> installPrometheus(PROMETHEUS_CHART_VERSION,
255263
domainNamespace,
256264
domainUid), "Failed to install Prometheus");
265+
266+
String promURL = prometheusReleaseName + "-server." + monitoringNS + ".svc.cluster.local";
257267
prometheusAdapterHelmParams = assertDoesNotThrow(() -> installAndVerifyPrometheusAdapter(
258268
prometheusAdapterReleaseName,
259-
monitoringNS, K8S_NODEPORT_HOST, nodeportPrometheus), "Failed to install Prometheus Adapter");
269+
monitoringNS, promURL, 80), "Failed to install Prometheus Adapter");
270+
260271
// wait till prometheus adapter could get the current custom metrics
261272
// total_opened_sessions_myear_app to make sure it is ready
262273
testUntil(withStandardRetryPolicy,
@@ -272,17 +283,19 @@ void testHPAWithCustomMetrics() {
272283
String ingressClassName = nginxHelmParams.getIngressClassName();
273284
List<String> ingressHostList
274285
= createIngressForDomainAndVerify(domainUid, domainNamespace, 0, clusterNameMsPortMap,
275-
false, ingressClassName, false, 0);
286+
true, ingressClassName, false, 0);
276287
// create hpa with custom metrics
277288
createHPA();
278289
//invoke app 20 times to generate metrics with number of opened sessions > 5
290+
String host = formatIPv6Host(K8S_NODEPORT_HOST);
291+
String hostPort = OKE_CLUSTER_PRIVATEIP ? ingressIP : host + ":" + nodeportshttp;
279292
String curlCmd =
280-
String.format("curl --silent --show-error --noproxy '*' -H 'host: %s' http://%s:%s@%s:%s/" + SESSMIGT_APP_URL,
293+
String.format("curl --silent --show-error --noproxy '*' -H 'host: %s' http://%s:%s@%s/" + SESSMIGT_APP_URL,
281294
ingressHostList.get(0),
282295
ADMIN_USERNAME_DEFAULT,
283296
ADMIN_PASSWORD_DEFAULT,
284-
K8S_NODEPORT_HOST,
285-
nodeportshttp);
297+
hostPort);
298+
286299
logger.info("Executing curl command " + curlCmd);
287300
for (int i = 0; i < 50; i++) {
288301
assertDoesNotThrow(() -> ExecCommand.exec(curlCmd));
@@ -427,6 +440,9 @@ private void installPrometheus(String promChartVersion,
427440
assertNotNull(promHelmParams, " Failed to install prometheus");
428441
prometheusDomainRegexValue = prometheusRegexValue;
429442
nodeportPrometheus = promHelmParams.getNodePortServer();
443+
String ingressClassName = nginxHelmParams.getIngressClassName();
444+
createIngressPathRouting(monitoringNS, "/",
445+
prometheusReleaseName + "-server", 80, ingressClassName);
430446
}
431447
//if prometheus already installed change CM for specified domain
432448
if (!prometheusRegexValue.equals(prometheusDomainRegexValue)) {

integration-tests/src/test/java/oracle/weblogic/kubernetes/ItIstioMonitoringExporter.java

Lines changed: 80 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) 2021, 2023, Oracle and/or its affiliates.
1+
// Copyright (c) 2021, 2024, Oracle and/or its affiliates.
22
// Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl.
33

44
package oracle.weblogic.kubernetes;
@@ -16,6 +16,7 @@
1616
import oracle.weblogic.kubernetes.annotations.Namespaces;
1717
import oracle.weblogic.kubernetes.logging.LoggingFacade;
1818
import oracle.weblogic.kubernetes.utils.ExecResult;
19+
import org.junit.jupiter.api.AfterAll;
1920
import org.junit.jupiter.api.BeforeAll;
2021
import org.junit.jupiter.api.DisplayName;
2122
import org.junit.jupiter.api.Tag;
@@ -24,19 +25,27 @@
2425
import static oracle.weblogic.kubernetes.TestConstants.ADMIN_PASSWORD_DEFAULT;
2526
import static oracle.weblogic.kubernetes.TestConstants.ADMIN_USERNAME_DEFAULT;
2627
import static oracle.weblogic.kubernetes.TestConstants.K8S_NODEPORT_HOST;
28+
import static oracle.weblogic.kubernetes.TestConstants.OKE_CLUSTER;
29+
import static oracle.weblogic.kubernetes.TestConstants.OKE_CLUSTER_PRIVATEIP;
2730
import static oracle.weblogic.kubernetes.TestConstants.RESULTS_ROOT;
2831
import static oracle.weblogic.kubernetes.TestConstants.TEST_IMAGES_REPO_SECRET_NAME;
2932
import static oracle.weblogic.kubernetes.TestConstants.WEBLOGIC_SLIM;
3033
import static oracle.weblogic.kubernetes.actions.ActionConstants.MODEL_DIR;
3134
import static oracle.weblogic.kubernetes.actions.ActionConstants.RESOURCE_DIR;
3235
import static oracle.weblogic.kubernetes.actions.TestActions.addLabelsToNamespace;
36+
import static oracle.weblogic.kubernetes.actions.TestActions.deleteImage;
3337
import static oracle.weblogic.kubernetes.utils.ApplicationUtils.checkAppUsingHostHeader;
3438
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.checkServiceExists;
3539
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.createTestWebAppWarFile;
40+
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.formatIPv6Host;
3641
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.getImageBuilderExtraArgs;
3742
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.getNextFreePort;
43+
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.getServiceExtIPAddrtOke;
44+
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.startPortForwardProcess;
45+
import static oracle.weblogic.kubernetes.utils.CommonTestUtils.stopPortForwardProcess;
3846
import static oracle.weblogic.kubernetes.utils.ConfigMapUtils.createConfigMapAndVerify;
3947
import static oracle.weblogic.kubernetes.utils.DeployUtil.deployToClusterUsingRest;
48+
import static oracle.weblogic.kubernetes.utils.DeployUtil.deployUsingRest;
4049
import static oracle.weblogic.kubernetes.utils.DomainUtils.createDomainAndVerify;
4150
import static oracle.weblogic.kubernetes.utils.FileUtils.generateFileFromTemplate;
4251
import static oracle.weblogic.kubernetes.utils.ImageUtils.createMiiImageAndVerify;
@@ -54,6 +63,7 @@
5463
import static oracle.weblogic.kubernetes.utils.MonitoringUtils.editPrometheusCM;
5564
import static oracle.weblogic.kubernetes.utils.OperatorUtils.installAndVerifyOperator;
5665
import static oracle.weblogic.kubernetes.utils.PodUtils.checkPodReady;
66+
import static oracle.weblogic.kubernetes.utils.PodUtils.getPodName;
5767
import static oracle.weblogic.kubernetes.utils.SecretUtils.createSecretWithUsernamePassword;
5868
import static oracle.weblogic.kubernetes.utils.ThreadSafeLogger.getLogger;
5969
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
@@ -63,14 +73,15 @@
6373

6474
@DisplayName("Test the monitoring WebLogic Domain via istio provided Prometheus")
6575
@IntegrationTest
66-
@Tag("oke-parallel")
76+
@Tag("oke-gate")
6777
@Tag("kind-parallel")
6878
@Tag("olcne-mrg")
6979
class ItIstioMonitoringExporter {
7080

7181
private static String opNamespace = null;
7282
private static String domain1Namespace = null;
7383
private static String domain2Namespace = null;
84+
private static String nginxNamespace = null;
7485

7586
private String domain1Uid = "istio1-mii";
7687
private String domain2Uid = "istio2-mii";
@@ -79,14 +90,22 @@ class ItIstioMonitoringExporter {
7990
private final String workManagerName = "newWM";
8091
private final int replicaCount = 2;
8192
private static int prometheusPort;
93+
private static final String istioNamespace = "istio-system";
94+
private static final String istioIngressServiceName = "istio-ingressgateway";
8295

8396
private boolean isPrometheusDeployed = false;
97+
private boolean isPrometheusPortForward = false;
8498
private static LoggingFacade logger = null;
8599
private static String oldRegex;
100+
private static String miiImageSideCar = null;
101+
private static String miiImageWebApp = null;
102+
private static String exporterImage = null;
86103
private static String sessionAppPrometheusSearchKey =
87104
"wls_servlet_invocation_total_count%7Bapp%3D%22myear%22%7D%5B15s%5D";
88105

89106
private static String testWebAppWarLoc = null;
107+
private static String ingressIP = null;
108+
private static String hostPortPrometheus = null;
90109

91110
/**
92111
* Install Operator.
@@ -109,6 +128,7 @@ public static void initAll(@Namespaces(3) List<String> namespaces) {
109128
assertNotNull(namespaces.get(2), "Namespace list is null");
110129
domain2Namespace = namespaces.get(2);
111130

131+
112132
// Label the domain/operator namespace with istio-injection=enabled
113133
Map<String, String> labelMap = new HashMap<>();
114134
labelMap.put("istio-injection", "enabled");
@@ -141,10 +161,10 @@ public static void initAll(@Namespaces(3) List<String> namespaces) {
141161
void testIstioPrometheusViaExporterWebApp() {
142162
assertDoesNotThrow(() -> downloadMonitoringExporterApp(RESOURCE_DIR
143163
+ "/exporter/exporter-config.yaml", RESULTS_ROOT), "Failed to download monitoring exporter application");
144-
String miiImage = createAndVerifyMiiImageWithMonitoringExporter(RESULTS_ROOT + "/wls-exporter.war",
164+
miiImageWebApp = createAndVerifyMiiImageWithMonitoringExporter(RESULTS_ROOT + "/wls-exporter.war",
145165
MODEL_DIR + "/model.monexp.yaml");
146166
String managedServerPrefix = domain1Uid + "-cluster-1-managed-server";
147-
assertDoesNotThrow(() -> setupIstioModelInImageDomain(miiImage,
167+
assertDoesNotThrow(() -> setupIstioModelInImageDomain(miiImageWebApp,
148168
domain1Namespace,domain1Uid, managedServerPrefix), "setup for istio based domain failed");
149169
assertDoesNotThrow(() -> deployPrometheusAndVerify(domain1Namespace, domain1Uid, sessionAppPrometheusSearchKey),
150170
"failed to fetch expected metrics from Prometheus using monitoring exporter webapp");
@@ -172,21 +192,21 @@ void testIstioPrometheusWithSideCar() {
172192

173193
// build the model file list
174194
final List<String> modelList = Collections.singletonList(MODEL_DIR + "/model.sessmigr.yaml");
175-
String miiImage =
195+
miiImageSideCar =
176196
createMiiImageAndVerify("miimonexp-istio-image", modelList, appList);
177197

178198
// repo login and push image to registry if necessary
179-
imageRepoLoginAndPushImageToRegistry(miiImage);
199+
imageRepoLoginAndPushImageToRegistry(miiImageSideCar);
180200

181201
String monitoringExporterSrcDir = Paths.get(RESULTS_ROOT, "monitoringexp", "srcdir").toString();
182202
cloneMonitoringExporter(monitoringExporterSrcDir);
183-
String exporterImage = assertDoesNotThrow(() ->
203+
exporterImage = assertDoesNotThrow(() ->
184204
buildMonitoringExporterCreateImageAndPushToRepo(monitoringExporterSrcDir, "exporter",
185205
domain2Namespace, TEST_IMAGES_REPO_SECRET_NAME, getImageBuilderExtraArgs()),
186206
"Failed to create image for exporter");
187207
String exporterConfig = RESOURCE_DIR + "/exporter/exporter-config.yaml";
188208
String managedServerPrefix = domain2Uid + "-managed-server";
189-
assertDoesNotThrow(() -> setupIstioModelInImageDomain(miiImage, domain2Namespace, domain2Uid, exporterConfig,
209+
assertDoesNotThrow(() -> setupIstioModelInImageDomain(miiImageSideCar, domain2Namespace, domain2Uid, exporterConfig,
190210
exporterImage, managedServerPrefix), "setup for istio based domain failed");
191211
assertDoesNotThrow(() -> deployPrometheusAndVerify(domain2Namespace, domain2Uid, sessionAppPrometheusSearchKey),
192212
"failed to fetch expected metrics from Prometheus using monitoring exporter sidecar");
@@ -198,18 +218,50 @@ private void deployPrometheusAndVerify(String domainNamespace, String domainUid,
198218
String.valueOf(prometheusPort)), "failed to install istio prometheus");
199219
isPrometheusDeployed = true;
200220
oldRegex = String.format("regex: %s;%s", domainNamespace, domainUid);
221+
//verify metrics via prometheus
222+
String host = formatIPv6Host(K8S_NODEPORT_HOST);
223+
224+
// In internal OKE env, use Istio EXTERNAL-IP; in non-OKE env, use K8S_NODEPORT_HOST + ":" + istioIngressPort
225+
hostPortPrometheus = getServiceExtIPAddrtOke(istioIngressServiceName, istioNamespace) != null
226+
? getServiceExtIPAddrtOke(istioIngressServiceName, istioNamespace) : host + ":" + prometheusPort;
227+
228+
if (OKE_CLUSTER_PRIVATEIP) {
229+
String localhost = "localhost";
230+
// Forward the non-ssl port 9090
231+
String podName = getPodName(istioNamespace, "prometheus-");
232+
String forwardPort = startPortForwardProcess(localhost, istioNamespace, 9090, podName);
233+
assertNotNull(forwardPort, "port-forward fails to assign local port");
234+
logger.info("Forwarded local port is {0}", forwardPort);
235+
hostPortPrometheus = localhost + ":" + forwardPort;
236+
isPrometheusPortForward = true;
237+
238+
}
201239
} else {
202240
String newRegex = String.format("regex: %s;%s", domainNamespace, domainUid);
203241
assertDoesNotThrow(() -> editPrometheusCM(oldRegex, newRegex, "istio-system", "prometheus"),
204242
"Can't modify Prometheus CM, not possible to monitor " + domainUid);
205243
}
206-
//verify metrics via prometheus
207-
String host = K8S_NODEPORT_HOST;
208-
if (host.contains(":")) {
209-
host = "[" + host + "]";
210-
}
244+
211245
checkMetricsViaPrometheus(searchKey, "sessmigr",
212-
host + ":" + prometheusPort);
246+
hostPortPrometheus);
247+
}
248+
249+
@AfterAll
250+
public void tearDownAll() {
251+
252+
// delete mii domain images created for parameterized test
253+
if (miiImageWebApp != null) {
254+
deleteImage(miiImageWebApp);
255+
}
256+
if (miiImageSideCar != null) {
257+
deleteImage(miiImageSideCar);
258+
}
259+
if (exporterImage != null) {
260+
deleteImage(exporterImage);
261+
}
262+
if (OKE_CLUSTER_PRIVATEIP) {
263+
stopPortForwardProcess(istioNamespace);
264+
}
213265
}
214266

215267
/**
@@ -338,15 +390,16 @@ private void setupIstioModelInImageDomain(String miiImage, String domainNamespac
338390

339391
int istioIngressPort = getIstioHttpIngressPort();
340392
logger.info("Istio Ingress Port is {0}", istioIngressPort);
393+
String host = formatIPv6Host(K8S_NODEPORT_HOST);
394+
395+
// In internal OKE env, use Istio EXTERNAL-IP; in non-OKE env, use K8S_NODEPORT_HOST + ":" + istioIngressPort
396+
String hostAndPort = getServiceExtIPAddrtOke(istioIngressServiceName, istioNamespace) != null
397+
? getServiceExtIPAddrtOke(istioIngressServiceName, istioNamespace) : host + ":" + istioIngressPort;
341398

342399
// We can not verify Rest Management console thru Adminstration NodePort
343400
// in istio, as we can not enable Adminstration NodePort
344401
if (!WEBLOGIC_SLIM) {
345-
String host = K8S_NODEPORT_HOST;
346-
if (host.contains(":")) {
347-
host = "[" + host + "]";
348-
}
349-
String consoleUrl = "http://" + host + ":" + istioIngressPort + "/console/login/LoginForm.jsp";
402+
String consoleUrl = "http://" + hostAndPort + "/console/login/LoginForm.jsp";
350403
boolean checkConsole =
351404
checkAppUsingHostHeader(consoleUrl, domainNamespace + ".org");
352405
assertTrue(checkConsole, "Failed to access WebLogic console");
@@ -356,20 +409,21 @@ private void setupIstioModelInImageDomain(String miiImage, String domainNamespac
356409
}
357410

358411
Path archivePath = Paths.get(testWebAppWarLoc);
359-
ExecResult result = null;
360-
result = deployToClusterUsingRest(K8S_NODEPORT_HOST,
412+
String target = "{identity: [clusters,'" + clusterName + "']}";
413+
ExecResult result = OKE_CLUSTER
414+
? deployUsingRest(hostAndPort, ADMIN_USERNAME_DEFAULT, ADMIN_PASSWORD_DEFAULT,
415+
target, archivePath, domainNamespace + ".org", "testwebapp")
416+
: deployToClusterUsingRest(K8S_NODEPORT_HOST,
361417
String.valueOf(istioIngressPort),
362418
ADMIN_USERNAME_DEFAULT, ADMIN_PASSWORD_DEFAULT,
363419
clusterName, archivePath, domainNamespace + ".org", "testwebapp");
420+
364421
assertNotNull(result, "Application deployment failed");
365422
logger.info("Application deployment returned {0}", result.toString());
366423
assertEquals("202", result.stdout(), "Deployment didn't return HTTP status code 202");
367424

368-
String host = K8S_NODEPORT_HOST;
369-
if (host.contains(":")) {
370-
host = "[" + host + "]";
371-
}
372-
String url = "http://" + host + ":" + istioIngressPort + "/testwebapp/index.jsp";
425+
426+
String url = "http://" + hostAndPort + "/testwebapp/index.jsp";
373427
logger.info("Application Access URL {0}", url);
374428
}
375429

integration-tests/src/test/java/oracle/weblogic/kubernetes/ItMonitoringExporterSamples.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ void testEndToEndViaChart() throws Exception {
275275
if (!OKD) {
276276
ingressHost2List
277277
= createIngressForDomainAndVerify(domain2Uid, domain2Namespace, 0, clusterNameMsPortMap,
278-
false, nginxHelmParams.getIngressClassName(), false, 0);
278+
true, nginxHelmParams.getIngressClassName(), false, 0);
279279
logger.info("verify access to Monitoring Exporter");
280280
if (OKE_CLUSTER_PRIVATEIP) {
281281
verifyMonExpAppAccessThroughNginx(ingressHost2List.get(0), managedServersCount, ingressIP);

0 commit comments

Comments
 (0)