diff --git a/.doc_gen/metadata/neptune_metadata.yaml b/.doc_gen/metadata/neptune_metadata.yaml index 821bd4a020c..4b2595942be 100644 --- a/.doc_gen/metadata/neptune_metadata.yaml +++ b/.doc_gen/metadata/neptune_metadata.yaml @@ -5,6 +5,15 @@ neptune_Hello: synopsis: get started using &neptune;. category: Hello languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.hello.main Python: versions: - sdk_version: 3 @@ -131,6 +140,15 @@ neptune_ExecuteGremlinQuery: neptune: {ExecuteGremlinQuery} neptune_DeleteDBSubnetGroup: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.delete.subnet.group.main Python: versions: - sdk_version: 3 @@ -152,6 +170,15 @@ neptune_DeleteDBSubnetGroup: neptune: {DeleteDBSubnetGroup} neptune_DeleteDBCluster: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.delete.cluster.main Python: versions: - sdk_version: 3 @@ -173,6 +200,15 @@ neptune_DeleteDBCluster: neptune: {DeleteDBCluster} neptune_DeleteDBInstance: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.delete.instance.main Python: versions: - sdk_version: 3 @@ -194,6 +230,15 @@ neptune_DeleteDBInstance: neptune: {DeleteDBInstance} neptune_StartDBCluster: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.start.cluster.main Python: versions: - sdk_version: 3 @@ -215,6 +260,15 @@ neptune_StartDBCluster: neptune: {StartDBCluster} neptune_StopDBCluster: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.stop.cluster.main Python: versions: - sdk_version: 3 @@ -236,6 +290,15 @@ neptune_StopDBCluster: neptune: {StopDBCluster} neptune_DescribeDBClusters: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.describe.cluster.main Python: versions: - sdk_version: 3 @@ -257,6 +320,15 @@ neptune_DescribeDBClusters: neptune: {DescribeDBClusters} neptune_DescribeDBInstances: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.describe.dbinstance.main Python: versions: - sdk_version: 3 @@ -278,6 +350,15 @@ neptune_DescribeDBInstances: neptune: {DescribeDBInstances} neptune_CreateDBInstance: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.create.dbinstance.main Python: versions: - sdk_version: 3 @@ -299,6 +380,15 @@ neptune_CreateDBInstance: neptune: {CreateDBInstance} neptune_CreateDBCluster: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.create.cluster.main Python: versions: - sdk_version: 3 @@ -320,6 +410,15 @@ neptune_CreateDBCluster: neptune: {CreateDBCluster} neptune_CreateDBSubnetGroup: languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.create.subnet.main Python: versions: - sdk_version: 3 @@ -351,6 +450,15 @@ neptune_Scenario: - Delete the &neptune; Assets. category: Basics languages: + Kotlin: + versions: + - sdk_version: 1 + github: kotlin/services/neptune + sdkguide: + excerpts: + - description: + snippet_tags: + - neptune.kotlin.scenario.main Python: versions: - sdk_version: 3 diff --git a/kotlin/services/neptune/.gitignore b/kotlin/services/neptune/.gitignore new file mode 100644 index 00000000000..b63da4551b2 --- /dev/null +++ b/kotlin/services/neptune/.gitignore @@ -0,0 +1,42 @@ +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/kotlin/services/neptune/README.md b/kotlin/services/neptune/README.md new file mode 100644 index 00000000000..9a9e416756a --- /dev/null +++ b/kotlin/services/neptune/README.md @@ -0,0 +1,123 @@ +# Neptune code examples for the SDK for Kotlin + +## Overview + +Shows how to use the AWS SDK for Kotlin to work with Amazon Neptune. + + + + +_Neptune is a serverless graph database designed for superior scalability and availability._ + +## ⚠ Important + +* Running this code might result in charges to your AWS account. For more details, see [AWS Pricing](https://aws.amazon.com/pricing/) and [Free Tier](https://aws.amazon.com/free/). +* Running the tests might result in charges to your AWS account. +* We recommend that you grant your code least privilege. At most, grant only the minimum permissions required to perform the task. For more information, see [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege). +* This code is not tested in every AWS Region. For more information, see [AWS Regional Services](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services). + + + + +## Code examples + +### Prerequisites + +For prerequisites, see the [README](../../README.md#Prerequisites) in the `kotlin` folder. + + + + + +### Get started + +- [Hello Neptune](src/main/java/com/example/neptune/HelloNeptune.kt#L9) (`DescribeDBClustersPaginator`) + + +### Basics + +Code examples that show you how to perform the essential operations within a service. + +- [Learn the basics](src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt) + + +### Single actions + +Code excerpts that show you how to call individual service functions. + +- [CreateDBCluster](src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt#L546) +- [CreateDBInstance](src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt#L510) +- [CreateDBSubnetGroup](src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt#L583) +- [DeleteDBCluster](src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt#L204) +- [DeleteDBInstance](src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt#L275) +- [DeleteDBSubnetGroup](src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt#L179) +- [DescribeDBClusters](src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt#L400) +- [DescribeDBInstances](src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt#L447) +- [StartDBCluster](src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt#L344) +- [StopDBCluster](src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt#L372) + + + + + +## Run the examples + +### Instructions + + + + + +#### Hello Neptune + +This example shows you how to get started using Neptune. + + +#### Learn the basics + +This example shows you how to do the following: + +- Create an Amazon Neptune Subnet Group. +- Create an Neptune Cluster. +- Create an Neptune Instance. +- Check the status of the Neptune Instance. +- Show Neptune cluster details. +- Stop the Neptune cluster. +- Start the Neptune cluster. +- Delete the Neptune Assets. + + + + + + + + + +### Tests + +⚠ Running tests might result in charges to your AWS account. + + +To find instructions for running these tests, see the [README](../../README.md#Tests) +in the `kotlin` folder. + + + + + + +## Additional resources + +- [Neptune User Guide](https://docs.aws.amazon.com/neptune/latest/userguide/intro.html) +- [Neptune API Reference](https://docs.aws.amazon.com/neptune/latest/apiref/Welcome.html) +- [SDK for Kotlin Neptune reference](https://sdk.amazonaws.com/kotlin/api/latest/dynamodb/index.html) + + + + +--- + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +SPDX-License-Identifier: Apache-2.0 diff --git a/kotlin/services/neptune/build.gradle.kts b/kotlin/services/neptune/build.gradle.kts new file mode 100644 index 00000000000..c75329475d5 --- /dev/null +++ b/kotlin/services/neptune/build.gradle.kts @@ -0,0 +1,56 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + kotlin("jvm") version "1.9.0" + application +} + +group = "me.scmacdon" +version = "1.0-SNAPSHOT" + +java { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +buildscript { + repositories { + maven("https://plugins.gradle.org/m2/") + } + + dependencies { + classpath("org.jlleitschuh.gradle:ktlint-gradle:10.3.0") + } +} + +repositories { + mavenCentral() +} +apply(plugin = "org.jlleitschuh.gradle.ktlint") + +dependencies { + implementation(platform("aws.sdk.kotlin:bom:1.3.112")) + implementation("aws.sdk.kotlin:neptune") + implementation("aws.sdk.kotlin:ec2") + implementation("aws.smithy.kotlin:http-client-engine-okhttp") + implementation("aws.smithy.kotlin:http-client-engine-crt") + implementation("com.google.code.gson:gson:2.10") + testImplementation("org.junit.jupiter:junit-jupiter:5.9.0") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4") + implementation("org.slf4j:slf4j-api:2.0.15") + implementation("org.slf4j:slf4j-simple:2.0.15") +} +tasks.withType { + kotlinOptions.jvmTarget = "17" +} + +tasks.test { + useJUnitPlatform() // Use JUnit 5 for running tests + testLogging { + events("passed", "skipped", "failed") + } + + // Define the test source set + testClassesDirs += files("build/classes/kotlin/test") + classpath += files("build/classes/kotlin/main", "build/resources/main") +} diff --git a/kotlin/services/neptune/settings.gradle.kts b/kotlin/services/neptune/settings.gradle.kts new file mode 100644 index 00000000000..b3b89751cd8 --- /dev/null +++ b/kotlin/services/neptune/settings.gradle.kts @@ -0,0 +1,9 @@ +pluginManagement { + plugins { + kotlin("jvm") version "2.1.10" + } +} +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} +rootProject.name = "neptune" diff --git a/kotlin/services/neptune/src/main/java/com/example/neptune/HelloNeptune.kt b/kotlin/services/neptune/src/main/java/com/example/neptune/HelloNeptune.kt new file mode 100644 index 00000000000..475fd3acfbe --- /dev/null +++ b/kotlin/services/neptune/src/main/java/com/example/neptune/HelloNeptune.kt @@ -0,0 +1,43 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.neptune + +import aws.sdk.kotlin.services.neptune.NeptuneClient +import aws.sdk.kotlin.services.neptune.model.DescribeDbClustersRequest + +// snippet-start:[neptune.kotlin.hello.main] +/** + * Before running this Kotlin code example, set up your development environment, including your credentials. + * + * For more information, see the following documentation topic: + * + * https://docs.aws.amazon.com/sdk-for-kotlin/latest/developer-guide/setup.html + */ +suspend fun main(args: Array) { + print("Hello Amazon Neptune") + listDBClusters() +} + +/** + * List details of DB clusters in Amazon Neptune. + */ +public suspend fun listDBClusters() { + val request = DescribeDbClustersRequest { + maxRecords = 20 + } + + NeptuneClient.fromEnvironment { region = "us-east-1" }.use { neptuneClient -> + neptuneClient.describeDbClusters(request) + val response = neptuneClient.describeDbClusters(request) + response.dbClusters?.forEach { cluster -> + println("Cluster Identifier: ${cluster.dbClusterIdentifier}") + println("Status: ${cluster.status}") + println("Endpoint: ${cluster.endpoint}") + println("Engine: ${cluster.engine}") + println("Engine Version: ${cluster.engineVersion}") + println("---") + } + } +} +// snippet-end:[neptune.kotlin.hello.main] diff --git a/kotlin/services/neptune/src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt b/kotlin/services/neptune/src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt new file mode 100644 index 00000000000..c95d0d7d3de --- /dev/null +++ b/kotlin/services/neptune/src/main/java/com/example/neptune/scenerio/NeptuneScenario.kt @@ -0,0 +1,666 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package com.example.neptune.scenerio + +import aws.sdk.kotlin.services.ec2.Ec2Client +import aws.sdk.kotlin.services.ec2.model.DescribeSubnetsRequest +import aws.sdk.kotlin.services.ec2.model.DescribeVpcsRequest +import aws.sdk.kotlin.services.ec2.model.Filter +import aws.sdk.kotlin.services.neptune.NeptuneClient +import aws.sdk.kotlin.services.neptune.model.CreateDbClusterRequest +import aws.sdk.kotlin.services.neptune.model.CreateDbInstanceRequest +import aws.sdk.kotlin.services.neptune.model.CreateDbSubnetGroupRequest +import aws.sdk.kotlin.services.neptune.model.DbClusterNotFoundFault +import aws.sdk.kotlin.services.neptune.model.DbInstanceNotFoundFault +import aws.sdk.kotlin.services.neptune.model.DbSubnetGroupNotFoundFault +import aws.sdk.kotlin.services.neptune.model.DbSubnetGroupQuotaExceededFault +import aws.sdk.kotlin.services.neptune.model.DeleteDbClusterRequest +import aws.sdk.kotlin.services.neptune.model.DeleteDbInstanceRequest +import aws.sdk.kotlin.services.neptune.model.DeleteDbSubnetGroupRequest +import aws.sdk.kotlin.services.neptune.model.DescribeDbClustersRequest +import aws.sdk.kotlin.services.neptune.model.DescribeDbInstancesRequest +import aws.sdk.kotlin.services.neptune.model.NeptuneException +import aws.sdk.kotlin.services.neptune.model.StartDbClusterRequest +import aws.sdk.kotlin.services.neptune.model.StopDbClusterRequest +import kotlinx.coroutines.delay +import java.time.Duration +import java.util.Scanner + +// snippet-start:[neptune.kotlin.scenario.main] +val DASHES = String(CharArray(80)).replace("\u0000", "-") +var scanner = Scanner(System.`in`) + +/** + * Before running this Kotlin code example, set up your development environment, including your credentials. + * + * For more information, see the following documentation topic: + * + * https://docs.aws.amazon.com/sdk-for-kotlin/latest/developer-guide/setup.html + */ +suspend fun main() { + val subnetGroupName = "neptuneSubnetGroup" + val clusterName = "neptuneCluster" + val dbInstanceId = "neptuneDB" + val client = NeptuneClient.fromEnvironment { region = "us-east-1" } + + println( + """ + Amazon Neptune is a fully managed AWS graph database optimized for complex, connected datasets. + It supports property graphs (Gremlin, openCypher) + and RDF graphs (SPARQL), making it ideal for knowledge + graphs, fraud detection, social networks, + recommendations, and network management. + + Neptune handles provisioning, patching, backups, a + nd replication, offering high availability by default. + + Developers can build relationship-aware apps using the + AWS SDK for Kotlin and automate infrastructure with + NeptuneClient. + + Let's get started... + + """.trimIndent(), + ) + waitForInputToContinue(scanner) + runScenario(client, subnetGroupName, dbInstanceId, clusterName) + println( + """ + Thank you for checking out the Amazon Neptune Service Use demo. We hope you + learned something new, or got some inspiration for your own apps today. + For more AWS code examples, have a look at: + https://docs.aws.amazon.com/code-library/latest/ug/what-is-code-library.html + """.trimIndent(), + ) +} + +suspend fun runScenario( + client: NeptuneClient, + subnetGroupName: String, + dbInstanceId: String, + clusterName: String, +) { + try { + println(DASHES) + println("1. Create a Neptune DB Subnet Group") + println("The Neptune DB subnet group is used when launching a Neptune cluster") + waitForInputToContinue(scanner) + createSubnetGroup(client, subnetGroupName) + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("2. Create a Neptune Cluster") + println("A Neptune Cluster allows you to store and query highly connected datasets with low latency.") + waitForInputToContinue(scanner) + val dbClusterId = createDbCluster(client, clusterName) + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("3. Create a Neptune DB Instance") + println("In this step, we add a new database instance to the Neptune cluster") + waitForInputToContinue(scanner) + createDbInstance(client, dbInstanceId, dbClusterId) + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("4. Check the status of the Neptune DB Instance") + println( + """ + In this step, we will wait until the DB instance + becomes available. This may take around 10 minutes. + """.trimIndent(), + ) + waitForInputToContinue(scanner) + checkInstanceStatus(client, dbInstanceId, "available") + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("5. Show Neptune Cluster details") + waitForInputToContinue(scanner) + describeDBClusters(client, dbClusterId) + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("6. Stop the Amazon Neptune cluster") + println( + """ + Once stopped, this step polls the status + until the cluster is in a stopped state. + """.trimIndent(), + ) + waitForInputToContinue(scanner) + stopDBCluster(client, dbClusterId) + waitForClusterStatus(client, dbClusterId, "stopped") + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("7. Start the Amazon Neptune cluster") + println( + """ + Once started, this step polls the clusters + status until it's in an available state. + We will also poll the instance status. + """.trimIndent(), + ) + waitForInputToContinue(scanner) + startDBCluster(client, dbClusterId) + waitForClusterStatus(client, dbClusterId, "available") + checkInstanceStatus(client, dbInstanceId, "available") + waitForInputToContinue(scanner) + println(DASHES) + + println(DASHES) + println("8. Delete the Neptune Assets") + println("Would you like to delete the Neptune Assets? (y/n)") + val delAns = scanner.nextLine().trim() + if (delAns.equals("y", ignoreCase = true)) { + println("You selected to delete the Neptune assets.") + deleteDbInstance(client, dbInstanceId) + waitUntilInstanceDeleted(client, dbInstanceId) + deleteDBCluster(client, dbClusterId) + deleteDBSubnetGroup(client, subnetGroupName) + println("Neptune resources deleted successfully.") + } else { + println("You selected not to delete Neptune assets.") + } + } catch (e: Exception) { + println("An error occurred during the scenario: ${e.message}") + e.printStackTrace() + } +} + +// snippet-start:[neptune.kotlin.delete.subnet.group.main] +/** + * Deletes a subnet group. + * + * @param subnetGroupName the identifier of the subnet group to delete + * @return a {@link CompletableFuture} that completes when the cluster has been deleted + */ +suspend fun deleteDBSubnetGroup(neptuneClient: NeptuneClient, subnetGroupName: String) { + val request = DeleteDbSubnetGroupRequest { + dbSubnetGroupName = subnetGroupName + } + + try { + neptuneClient.deleteDbSubnetGroup(request) + println("Deleting Subnet Group: $subnetGroupName") + } catch (e: DbSubnetGroupNotFoundFault) { + println("The subnet group was not found: ${e.message}") + throw e + } catch (e: NeptuneException) { + println("Neptune exception occurred: ${e.message}") + throw e + } +} +// snippet-end:[neptune.kotlin.delete.subnet.group.main] + +// snippet-start:[neptune.kotlin.delete.cluster.main] +/** + * Deletes a DB instance. + * + * @param clusterId the identifier of the cluster to delete + */ +suspend fun deleteDBCluster(neptuneClient: NeptuneClient, clusterId: String) { + val request = DeleteDbClusterRequest { + dbClusterIdentifier = clusterId + skipFinalSnapshot = true + } + + try { + neptuneClient.deleteDbCluster(request) + println("️ Deleting DB Cluster: $clusterId") + } catch (e: DbClusterNotFoundFault) { + println("\nResource not found: ${e.message}") + throw e + } catch (e: NeptuneException) { + println("Neptune exception occurred : ${e.message}") + throw e + } +} +// snippet-end:[neptune.kotlin.delete.cluster.main] + +suspend fun waitUntilInstanceDeleted( + neptuneClient: NeptuneClient, + instanceId: String, + timeout: Duration = Duration.ofMinutes(20), + pollInterval: Duration = Duration.ofSeconds(10), +): Boolean { + println("Waiting for instance '$instanceId' to be deleted...") + + val startTime = System.currentTimeMillis() + + while (true) { + try { + val request = DescribeDbInstancesRequest { + dbInstanceIdentifier = instanceId + } + + val response = neptuneClient.describeDbInstances(request) + val status = response.dbInstances?.firstOrNull()?.dbInstanceStatus ?: "Unknown" + val elapsed = (System.currentTimeMillis() - startTime) / 1000 + print("\r Waiting: Instance $instanceId status: ${status.padEnd(10)} (${elapsed}s elapsed)") + System.out.flush() + } catch (e: NeptuneException) { + val errorCode = e.sdkErrorMetadata.errorCode + return if (errorCode == "DBInstanceNotFound") { + val elapsed = (System.currentTimeMillis() - startTime) / 1000 + println("\nInstance '$instanceId' deleted after ${elapsed}s.") + true + } else { + println("\nError polling DB instance '$instanceId': ${errorCode ?: "Unknown"} — ${e.message}") + false + } + } catch (e: Exception) { + println("\nUnexpected error while polling DB instance '$instanceId': ${e.message}") + return false + } + + val elapsedMs = System.currentTimeMillis() - startTime + if (elapsedMs > timeout.toMillis()) { + println("\nTimeout: Instance '$instanceId' was not deleted after ${timeout.toMinutes()} minutes.") + return false + } + + delay(pollInterval.toMillis()) + } +} + +// snippet-start:[neptune.kotlin.delete.instance.main] +/** + * Deletes the specified Amazon Neptune DB instance. + * + * @param neptuneClient The Neptune client used to perform the deletion. + * @param instanceId The identifier of the DB instance to be deleted. + * + * @throws DbInstanceNotFoundFault if the specified DB instance does not exist. + * @throws NeptuneException if any other error occurs during the deletion process. + */ +suspend fun deleteDbInstance(neptuneClient: NeptuneClient, instanceId: String) { + val request = DeleteDbInstanceRequest { + dbInstanceIdentifier = instanceId + skipFinalSnapshot = true + } + + try { + neptuneClient.deleteDbInstance(request) + println("Deleting DB Instance: $instanceId") + } catch (e: DbInstanceNotFoundFault) { + println("\nThe DB instance was not found: ${e.message}") + throw e + } catch (e: NeptuneException) { + println("Neptune exception occurred : ${e.message}") + throw e + } +} +// snippet-end:[neptune.kotlin.delete.instance.main] + +/** + * Waits for a Neptune cluster to reach a desired status. + * + * @param clusterId the ID of the Neptune cluster to monitor + * @param desiredStatus the desired status of the Neptune cluster + * @param timeout the maximum time to wait for the cluster to reach the desired status + * @param pollInterval the interval at which to check the cluster's status + * + */ +suspend fun waitForClusterStatus( + neptuneClient: NeptuneClient, + clusterId: String, + desiredStatus: String, + timeout: Duration = Duration.ofMinutes(20), + pollInterval: Duration = Duration.ofSeconds(10), +) { + println("Waiting for cluster $clusterId to reach status '$desiredStatus'...") + val startTime = System.currentTimeMillis() + while (true) { + val request = DescribeDbClustersRequest { + dbClusterIdentifier = clusterId + } + + val response = neptuneClient.describeDbClusters(request) + val currentStatus = response.dbClusters?.firstOrNull()?.status ?: "Unknown" + val elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000 + println("Elapsed: ${formatElapsedTime(elapsedSeconds.toInt()).padEnd(20)} Cluster status: ${currentStatus.padEnd(20)}") + System.out.flush() + if (currentStatus.equals(desiredStatus, ignoreCase = true)) { + println("Neptune cluster reached desired status $desiredStatus after ${formatElapsedTime(elapsedSeconds.toInt())}") + return + } + + if (System.currentTimeMillis() - startTime > timeout.toMillis()) { + throw RuntimeException("Timed out waiting for Neptune cluster to reach status: $desiredStatus") + } + delay(pollInterval.toMillis()) + } +} + +// snippet-start:[neptune.kotlin.start.cluster.main] +/** + * Starts the specified Amazon Neptune DB cluster. + * + * @param neptuneClient The Neptune client used to make the request. + * @param clusterIdentifier The unique identifier of the DB cluster to be started. + * + * @throws DbClusterNotFoundFault if the specified DB cluster does not exist. + * @throws NeptuneException if any other service-level error occurs. + */ +suspend fun startDBCluster(neptuneClient: NeptuneClient, clusterIdentifier: String) { + val request = StartDbClusterRequest { + dbClusterIdentifier = clusterIdentifier + } + + try { + neptuneClient.startDbCluster(request) + println("DB Cluster started : $clusterIdentifier") + } catch (e: DbClusterNotFoundFault) { + println("\nResource not found: ${e.message}") + throw e + } catch (e: NeptuneException) { + println("Neptune exception occurred : ${e.message}") + throw e + } +} +// snippet-end:[neptune.kotlin.start.cluster.main] + +// snippet-start:[neptune.kotlin.stop.cluster.main] +/** + * Stops an Amazon Neptune DB cluster. + * + * @param neptuneClient The Neptune client used to make the request. + * @param clusterIdentifier The unique identifier of the DB cluster to be started. + * + * @throws DbClusterNotFoundFault if the specified DB cluster does not exist. + * @throws NeptuneException if any other service-level error occurs. + */ +suspend fun stopDBCluster(neptuneClient: NeptuneClient, clusterIdentifier: String) { + val request = StopDbClusterRequest { + dbClusterIdentifier = clusterIdentifier + } + + try { + neptuneClient.stopDbCluster(request) + println("DB Cluster stopped: $clusterIdentifier") + } catch (e: DbClusterNotFoundFault) { + println("\nThe Neptune DB cluster was not found: ${e.message}") + throw e + } catch (e: NeptuneException) { + println("A Neptune exception occurred : ${e.message}") + throw e + } +} +// snippet-end:[neptune.kotlin.stop.cluster.main] + +// snippet-start:[neptune.kotlin.describe.cluster.main] +/** +* Describes the specified Amazon Neptune DB cluster and prints detailed cluster information. +* +* @param neptuneClient The Neptune client used to make the describe request. +* @param clusterId The identifier of the Neptune DB cluster to describe. +* +* @throws DbClusterNotFoundFault if the specified DB cluster does not exist. +* @throws NeptuneException if any other error occurs while describing the cluster. +*/ +suspend fun describeDBClusters(neptuneClient: NeptuneClient, clusterId: String) { + val request = DescribeDbClustersRequest { + dbClusterIdentifier = clusterId + } + + try { + val response = neptuneClient.describeDbClusters(request) + response.dbClusters?.forEach { cluster -> + println("Cluster Identifier: ${cluster.dbClusterIdentifier}") + println("Status: ${cluster.status}") + println("Engine: ${cluster.engine}") + println("Engine Version: ${cluster.engineVersion}") + println("Endpoint: ${cluster.endpoint}") + println("Reader Endpoint: ${cluster.readerEndpoint}") + println("Availability Zones: ${cluster.availabilityZones}") + println("Subnet Group: ${cluster.dbSubnetGroup}") + println("VPC Security Groups:") + cluster.vpcSecurityGroups?.forEach { vpcGroup -> + println(" - ${vpcGroup.vpcSecurityGroupId}") + } + println("Storage Encrypted: ${cluster.storageEncrypted}") + println("IAM DB Auth Enabled: ${cluster.iamDatabaseAuthenticationEnabled}") + println("Backup Retention Period: ${cluster.backupRetentionPeriod} days") + println("Preferred Backup Window: ${cluster.preferredBackupWindow}") + println("Preferred Maintenance Window: ${cluster.preferredMaintenanceWindow}") + println("------") + } + } catch (e: DbClusterNotFoundFault) { + println("\nThe Neptune DB cluster was not found: ${e.message}") + throw e + } catch (e: NeptuneException) { + println("Neptune exception occurred : ${e.message}") + throw e + } +} +// snippet-end:[neptune.kotlin.describe.cluster.main] + +// snippet-start:[neptune.kotlin.describe.dbinstance.main] +/** + * Polls the status of an Amazon Neptune DB instance until it reaches the desired status or times out. + * + * @param neptuneClient The Neptune client used to query instance status. + * @param instanceId The identifier of the DB instance to monitor. + * @param desiredStatus The status to wait for (e.g., "available"). + * @param pollInterval The interval between status checks. Default is 10 seconds. + * + * @throws DbInstanceNotFoundFault if the DB instance is not found and should not be retried. + * @throws NeptuneException if a service-level error occurs. + */ +suspend fun checkInstanceStatus( + neptuneClient: NeptuneClient, + instanceId: String, + desiredStatus: String, + pollInterval: Duration = Duration.ofSeconds(10), +) { + val startTime = System.currentTimeMillis() + println("Checking status for instance '$instanceId'...") + + try { + while (true) { + val request = DescribeDbInstancesRequest { + dbInstanceIdentifier = instanceId + } + + val currentStatus = try { + val response = neptuneClient.describeDbInstances(request) + response.dbInstances?.firstOrNull()?.dbInstanceStatus ?: "Unknown" + } catch (e: DbInstanceNotFoundFault) { + println("\nInstance '$instanceId' not found. Retrying...") + delay(pollInterval.toMillis()) + continue // retry loop + } + + val elapsedSeconds = (System.currentTimeMillis() - startTime) / 1000 + print("\rElapsed: ${formatElapsedTime(elapsedSeconds.toInt()).padEnd(20)} Status: ${currentStatus.padEnd(20)}") + System.out.flush() + + if (desiredStatus.equals(currentStatus, ignoreCase = true)) { + println("\nInstance reached desired status '$desiredStatus' after ${formatElapsedTime(elapsedSeconds.toInt())}.") + break + } + + delay(pollInterval.toMillis()) + } + } catch (e: NeptuneException) { + println("\nNeptune exception while checking instance status: ${e.message}") + throw e + } catch (e: Exception) { + println("\nUnexpected exception while checking instance status: ${e.message}") + throw e + } +} +// snippet-end:[neptune.kotlin.describe.dbinstance.main] + +private fun formatElapsedTime(totalSeconds: Int): String { + val minutes = totalSeconds / 60 + val seconds = totalSeconds % 60 + return String.format("%02d:%02d", minutes, seconds) +} + +// snippet-start:[neptune.kotlin.create.dbinstance.main] + +/** +* Creates a new Amazon Neptune DB instance and returns its identifier. +* +* @param neptuneClient The Neptune client used to send the create request. +* @param dbInstanceId The identifier to assign to the new DB instance. +* @param dbClusterId The identifier of the Neptune DB cluster to associate the instance with. +* @return The identifier of the created Neptune DB instance. +* +* @throws DbSubnetGroupQuotaExceededFault if the subnet group quota has been exceeded. +* @throws NeptuneException if any other error occurs during the creation of the instance. +*/ +suspend fun createDbInstance(neptuneClient: NeptuneClient, dbInstanceId: String, dbClusterId: String): String? { + val request = CreateDbInstanceRequest { + dbInstanceIdentifier = dbInstanceId + dbInstanceClass = "db.r5.large" + engine = "neptune" + dbClusterIdentifier = dbClusterId + } + + try { + val response = neptuneClient.createDbInstance(request) + val instanceId = response.dbInstance?.dbInstanceIdentifier + println("Created Neptune DB Instance: $instanceId") + return instanceId + } catch (e: DbSubnetGroupQuotaExceededFault) { + println("Quota exceeded when creating '$dbInstanceId': ${e.message}") + throw e + } catch (e: NeptuneException) { + println("Neptune exception when creating '$dbInstanceId': ${e.message}") + throw e + } +} +// snippet-end:[neptune.kotlin.create.dbinstance.main] + +// snippet-start:[neptune.kotlin.create.cluster.main] +/** + * Creates a new Amazon Neptune DB cluster and returns its identifier. + * + * @param neptuneClient The Neptune client used to perform the cluster creation. + * @param dbName The unique identifier to assign to the new DB cluster. + * @return The identifier of the created Neptune DB cluster. + * + * @throws DbSubnetGroupQuotaExceededFault if the subnet group quota is exceeded. + * @throws NeptuneException if a service-level error occurs during cluster creation. + * @throws RuntimeException if the cluster is created but no cluster ID is returned. + */ +suspend fun createDbCluster(neptuneClient: NeptuneClient, dbName: String): String { + val request = CreateDbClusterRequest { + dbClusterIdentifier = dbName + engine = "neptune" + deletionProtection = false + backupRetentionPeriod = 1 + } + + try { + val response = neptuneClient.createDbCluster(request) + val clusterId = response.dbCluster?.dbClusterIdentifier + ?: throw RuntimeException("Cluster creation succeeded but no ID returned.") + + println("DB Cluster created: $clusterId") + return clusterId + } catch (e: DbSubnetGroupQuotaExceededFault) { + println("Quota exceeded when creating '$dbName': ${e.message}") + throw e + } catch (e: NeptuneException) { + println("Neptune exception when creating '$dbName': ${e.message}") + throw e + } +} +// snippet-end:[neptune.kotlin.create.cluster.main] + +// snippet-start:[neptune.kotlin.create.subnet.main] +/** + * Creates an Amazon Neptune DB subnet group using the default VPC and its associated subnets. + * + * @param neptuneClient The Neptune client used to send the create request. + * @param groupName The name to assign to the new subnet group. + * + * @throws DbSubnetGroupQuotaExceededFault if the subnet group quota has been exceeded. + * @throws NeptuneException if a service-level error occurs during subnet group creation. + */ +suspend fun createSubnetGroup(neptuneClient: NeptuneClient, groupName: String) { + val vpcId = getDefaultVpcId() + val subnetList = getSubnetIds(vpcId) + + val request = CreateDbSubnetGroupRequest { + dbSubnetGroupName = groupName + dbSubnetGroupDescription = "Subnet group for Neptune cluster" + subnetIds = subnetList + } + + try { + val response = neptuneClient.createDbSubnetGroup(request) + val name = response.dbSubnetGroup?.dbSubnetGroupName + println("Subnet group created: $name") + } catch (e: DbSubnetGroupQuotaExceededFault) { + println("Quota exceeded when creating subnet group '$groupName': ${e.message}") + throw e + } catch (e: NeptuneException) { + println("Neptune exception when creating subnet group '$groupName': ${e.message}") + throw e + } +} +// snippet-end:[neptune.kotlin.create.subnet.main] + +suspend fun getDefaultVpcId(): String { + val myFilter = Filter { + name = "isDefault" + values = listOf("true") + } + + Ec2Client.fromEnvironment { region = "us-east-1" }.use { ec2Client -> + val request = DescribeVpcsRequest { + filters = listOf(myFilter) + } + + val response = ec2Client.describeVpcs(request) + val defaultVpcId = response.vpcs?.firstOrNull()?.vpcId + ?: throw RuntimeException("No default VPC found in this region.") + + println("Default VPC ID: $defaultVpcId") + return defaultVpcId + } +} + +suspend fun getSubnetIds(vpcId: String): List { + val myFilter = Filter { + name = "vpc-id" + values = listOf(vpcId) + } + Ec2Client.fromEnvironment { region = "us-east-1" }.use { ec2Client -> + val request = DescribeSubnetsRequest { + filters = listOf(myFilter) + } + val response = ec2Client.describeSubnets(request) + return response.subnets?.mapNotNull { it.subnetId } ?: emptyList() + } +} + +private fun waitForInputToContinue(scanner: Scanner) { + while (true) { + println("") + println("Enter 'c' followed by to continue:") + val input = scanner.nextLine() + + if (input.trim { it <= ' ' }.equals("c", ignoreCase = true)) { + println("Continuing with the program...") + println("") + break + } else { + println("Invalid input. Please try again.") + } + } +} +// snippet-end:[neptune.kotlin.scenario.main] diff --git a/kotlin/services/neptune/src/test/java/NeptuneTest.kt b/kotlin/services/neptune/src/test/java/NeptuneTest.kt new file mode 100644 index 00000000000..7fe118f777d --- /dev/null +++ b/kotlin/services/neptune/src/test/java/NeptuneTest.kt @@ -0,0 +1,153 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +import aws.sdk.kotlin.services.neptune.NeptuneClient +import com.example.neptune.scenerio.checkInstanceStatus +import com.example.neptune.scenerio.createDbCluster +import com.example.neptune.scenerio.createDbInstance +import com.example.neptune.scenerio.createSubnetGroup +import com.example.neptune.scenerio.deleteDBCluster +import com.example.neptune.scenerio.deleteDBSubnetGroup +import com.example.neptune.scenerio.deleteDbInstance +import com.example.neptune.scenerio.describeDBClusters +import com.example.neptune.scenerio.startDBCluster +import com.example.neptune.scenerio.stopDBCluster +import com.example.neptune.scenerio.waitForClusterStatus +import com.example.neptune.scenerio.waitUntilInstanceDeleted +import kotlinx.coroutines.runBlocking +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.MethodOrderer +import org.junit.jupiter.api.Order +import org.junit.jupiter.api.Tag +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import org.junit.jupiter.api.TestMethodOrder + +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@TestMethodOrder(MethodOrderer.OrderAnnotation::class) +class NeptuneTest { + private val subnetGroupName = "neptuneSubnetGroup65" + private val clusterName = "neptuneCluster65" + private val dbInstanceId = "neptuneDB65" + private var dbClusterId = "" + private lateinit var client: NeptuneClient + + @BeforeAll + fun setup() = runBlocking { + client = NeptuneClient.fromEnvironment { region = "us-east-1" } + } + + @Test + @Tag("IntegrationTest") + @Order(1) + fun testSubnetGroup() = runBlocking { + runCatching { + createSubnetGroup(client, subnetGroupName) + }.onFailure { + it.printStackTrace() + Assertions.fail("Subnet group creation failed: ${it.message}") + }.getOrThrow() + + println("Created Group: $subnetGroupName") + } + + @Test + @Tag("IntegrationTest") + @Order(2) + fun testCreateDbCluster() = runBlocking { + dbClusterId = runCatching { + createDbCluster(client, clusterName) + }.onFailure { + it.printStackTrace() + Assertions.fail("DB cluster creation failed: ${it.message}") + }.getOrThrow() + + Assertions.assertNotNull(dbClusterId, "Expected DB cluster ID to be non-null") + println("Created DB Cluster: $dbClusterId") + } + + @Test + @Tag("IntegrationTest") + @Order(3) + fun testCreateDbInstance() = runBlocking { + runCatching { + createDbInstance(client, dbInstanceId, dbClusterId) + }.onFailure { + it.printStackTrace() + Assertions.fail("DB Instance creation failed: ${it.message}") + }.getOrThrow() + println("Created DB Instance: $dbInstanceId") + } + + @Test + @Tag("IntegrationTest") + @Order(4) + fun testCheckInstanceStatus() = runBlocking { + runCatching { + checkInstanceStatus(client, dbInstanceId, "available") + }.onFailure { + it.printStackTrace() + Assertions.fail("Instance status check failed: ${it.message}") + }.getOrThrow() + println("Instance status check passed: $dbInstanceId") + } + + @Test + @Tag("IntegrationTest") + @Order(5) + fun testDescribeDBClusters() = runBlocking { + runCatching { + describeDBClusters(client, dbClusterId) + }.onFailure { + it.printStackTrace() + Assertions.fail("Describe Cluster failed: ${it.message}") + }.getOrThrow() + println("Describe cluster passed: $dbInstanceId") + } + + @Test + @Tag("IntegrationTest") + @Order(6) + fun testStopClusters() = runBlocking { + runCatching { + stopDBCluster(client, dbClusterId) + waitForClusterStatus(client, dbClusterId, "stopped") + }.onFailure { + it.printStackTrace() + Assertions.fail("Stopping the Cluster failed: ${it.message}") + }.getOrThrow() + println("Stopping the cluster passed: $dbInstanceId") + } + + @Test + @Tag("IntegrationTest") + @Order(7) + fun testStartClusters() = runBlocking { + runCatching { + startDBCluster(client, dbClusterId) + waitForClusterStatus(client, dbClusterId, "available") + checkInstanceStatus(client, dbInstanceId, "available") + }.onFailure { + it.printStackTrace() + Assertions.fail("Starting the Cluster failed: ${it.message}") + }.getOrThrow() + println("Starting the cluster passed: $dbInstanceId") + } + + @Test + @Tag("IntegrationTest") + @Order(8) + fun testDeleteResources() = runBlocking { + runCatching { + deleteDbInstance(client, dbInstanceId) + waitUntilInstanceDeleted(client, dbInstanceId) + deleteDBCluster(client, dbClusterId) + deleteDBSubnetGroup(client, subnetGroupName) + }.onFailure { + it.printStackTrace() + Assertions.fail("Deleting the resources failed: ${it.message}") + }.getOrThrow() + println("Deleting the resources passed: $dbInstanceId") + } +}