Skip to content

Commit

Permalink
Merge branch 'main' into update/aws
Browse files Browse the repository at this point in the history
  • Loading branch information
paul-butcher authored Jan 7, 2025
2 parents 8b21b1a + db9e344 commit 6adf950
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 59 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# CHANGELOG

## v32.43.3 - 2025-01-06

Update ElasticBuilder to generate and accept config case classes to enable
pushing typesafe config to the edge of consuming services.

## v32.43.2 - 2024-10-07

S3StreamWritable bug fix (replace `read` by `readNBytes`).
Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
val projectVersion = "32.43.2"
val projectVersion = "32.43.3"

Global / excludeLintKeys += composeNoBuild

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,35 @@ package weco.elasticsearch.typesafe
import com.sksamuel.elastic4s.ElasticClient
import com.typesafe.config.Config
import weco.elasticsearch.ElasticClientBuilder
import weco.typesafe.config.builders.EnrichConfig._

sealed trait ElasticConfig {
val host: String
val port: Int
val protocol: String
}

case class ElasticConfigUsernamePassword(
host: String,
port: Int,
protocol: String,
username: String,
password: String
) extends ElasticConfig

case class ElasticConfigApiKey(
host: String,
port: Int,
protocol: String,
apiKey: String
) extends ElasticConfig

object ElasticBuilder {
def buildElasticClient(config: Config,
namespace: String = ""): ElasticClient = {
import weco.typesafe.config.builders.EnrichConfig._

def buildElasticClientConfig(
config: Config,
namespace: String = ""
): ElasticConfig = {
val hostname = config.requireString(s"es.$namespace.host")
val port = config
.getIntOption(s"es.$namespace.port")
Expand All @@ -22,7 +46,7 @@ object ElasticBuilder {
config.getStringOption(s"es.$namespace.apikey")
) match {
case (Some(username), Some(password), None) =>
ElasticClientBuilder.create(
ElasticConfigUsernamePassword(
hostname,
port,
protocol,
Expand All @@ -31,10 +55,37 @@ object ElasticBuilder {
)
// Use an API key if specified, even if username/password are also present
case (_, _, Some(apiKey)) =>
ElasticClientBuilder.create(hostname, port, protocol, apiKey)
ElasticConfigApiKey(hostname, port, protocol, apiKey)
case _ =>
throw new Throwable(
s"You must specify username and password, or apikey, in the 'es.$namespace' config")
s"You must specify username and password, or apikey, in the 'es.$namespace' config"
)
}
}

def buildElasticClient(config: ElasticConfig): ElasticClient =
config match {
case ElasticConfigUsernamePassword(
hostname,
port,
protocol,
username,
password
) =>
ElasticClientBuilder.create(
hostname,
port,
protocol,
username,
password
)
case ElasticConfigApiKey(hostname, port, protocol, apiKey) =>
ElasticClientBuilder.create(hostname, port, protocol, apiKey)
}

def buildElasticClient(
config: Config,
namespace: String = ""
): ElasticClient =
buildElasticClient(buildElasticClientConfig(config, namespace))
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,116 @@ package weco.elasticsearch.typesafe
import com.typesafe.config.ConfigFactory
import org.scalatest.funspec.AnyFunSpec
import org.scalatest.matchers.should.Matchers
import weco.fixtures.RandomGenerators

class ElasticBuilderTest extends AnyFunSpec with Matchers {
it("builds a client when a username and password are specified") {
val config = ConfigFactory.parseString(
"""
|es.test.host = test.elastic
|es.test.username = test-username
|es.test.password = test-password
|""".stripMargin
)
class ElasticBuilderTest
extends AnyFunSpec
with Matchers
with RandomGenerators {

noException shouldBe thrownBy(
ElasticBuilder.buildElasticClient(config, "test")
val List(namespace, host, username, password, apiKey) =
1.to(5).map(_ => randomAlphanumeric()).toList
val defaultPort = 9200
val defaultProtocol = "http"

describe("when username and password are specified") {
val usernamePasswordConfig = ConfigFactory.parseString(
f"""
|es.$namespace.host = $host
|es.$namespace.username = $username
|es.$namespace.password = $password
|""".stripMargin
)

it("can build and accept ElasticConfigUsernamePassword") {
val elasticConfig = ElasticBuilder.buildElasticClientConfig(
usernamePasswordConfig,
namespace)

elasticConfig shouldBe ElasticConfigUsernamePassword(
host = host,
port = defaultPort,
protocol = defaultProtocol,
username = username,
password = password
)

noException shouldBe thrownBy(
ElasticBuilder.buildElasticClient(elasticConfig)
)
}

it("can build a client directly") {
noException shouldBe thrownBy(
ElasticBuilder.buildElasticClient(usernamePasswordConfig, namespace)
)
}
}

it("builds a client when an API key is specified") {
val config = ConfigFactory.parseString(
"""
|es.test.host = test.elastic
|es.test.apikey = test-key
|""".stripMargin
)
val apiKeyConfig = ConfigFactory.parseString(
f"""
|es.$namespace.host = $host
|es.$namespace.apikey = $apiKey
|""".stripMargin
)

noException shouldBe thrownBy(
ElasticBuilder.buildElasticClient(config, "test")
)
describe("when an API key is specified") {
it("can build and accept ElasticConfigApiKey") {
val elasticConfig =
ElasticBuilder.buildElasticClientConfig(apiKeyConfig, namespace)

elasticConfig shouldBe ElasticConfigApiKey(
host = host,
port = defaultPort,
protocol = defaultProtocol,
apiKey = apiKey
)

noException shouldBe thrownBy(
ElasticBuilder.buildElasticClient(elasticConfig)
)
}

it("builds a client directly") {
noException shouldBe thrownBy(
ElasticBuilder.buildElasticClient(apiKeyConfig, namespace)
)
}
}

describe("when an API key and username and password is specified") {
it("uses the API key") {
val config = ConfigFactory.parseString(
f"""
|es.$namespace.host = $host
|es.$namespace.username = $username
|es.$namespace.password = $password
|es.$namespace.apikey = $apiKey
|""".stripMargin
)

val elasticConfig =
ElasticBuilder.buildElasticClientConfig(config, namespace)

elasticConfig shouldBe ElasticConfigApiKey(
host = host,
port = defaultPort,
protocol = defaultProtocol,
apiKey = apiKey
)
}
}

it("errors if there is not enough config to build a client") {
val config = ConfigFactory.parseString(
"""
|es.test.host = test.elastic
|es.test.username = test-username
f"""
|es.$namespace.host = $host
|es.$namespace.username = $username
|""".stripMargin
)

a[Throwable] shouldBe thrownBy(
ElasticBuilder.buildElasticClient(config, "test")
ElasticBuilder.buildElasticClient(config, namespace)
)
}
}
3 changes: 2 additions & 1 deletion fixtures/src/test/scala/weco/fixtures/LocalResources.scala
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ trait LocalResources {
case Success(lines) => lines.mkString("\n")

case Failure(_: NullPointerException) if name.startsWith("/") =>
throw new RuntimeException(s"Could not find resource `$name`; try removing the leading slash")
throw new RuntimeException(
s"Could not find resource `$name`; try removing the leading slash")
case Failure(_: NullPointerException) =>
throw new RuntimeException(s"Could not find resource `$name`")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ class SNSMessageSenderTest

val result = sender.send("hello world")(
subject = "Sent from SNSMessageSenderTest",
destination = SNSConfig(topicArn = "arn:aws:sns:eu-west-1:012345678912:doesnotexist")
destination =
SNSConfig(topicArn = "arn:aws:sns:eu-west-1:012345678912:doesnotexist")
)

result shouldBe a[Failure[_]]
Expand Down
Loading

0 comments on commit 6adf950

Please sign in to comment.