diff --git a/build.sbt b/build.sbt index 15f085ee..a8f2ec67 100644 --- a/build.sbt +++ b/build.sbt @@ -9,7 +9,7 @@ inThisBuild( Developer("regis-leray", "Regis Leray", "regis.leray@gmail.com", url("https://github.com/regis-leray")) ), Test / fork := true, - parallelExecution in Test := false, + (Test / parallelExecution) := false, pgpPassphrase := sys.env.get("PGP_PASSWORD").map(_.toArray), pgpPublicRing := file("/tmp/public.asc"), pgpSecretRing := file("/tmp/secret.asc"), @@ -46,18 +46,18 @@ lazy val `zio-s3` = project lazy val docs = project .in(file("zio-s3-docs")) .settings( - skip.in(publish) := true, + skip / publish := true, moduleName := "zio-s3-docs", scalacOptions -= "-Yno-imports", scalacOptions -= "-Xfatal-warnings", libraryDependencies ++= Seq( "dev.zio" %% "zio" % zioVersion ), - unidocProjectFilter in (ScalaUnidoc, unidoc) := inProjects(`zio-s3`), - target in (ScalaUnidoc, unidoc) := (baseDirectory in LocalRootProject).value / "website" / "static" / "api", - cleanFiles += (target in (ScalaUnidoc, unidoc)).value, - docusaurusCreateSite := docusaurusCreateSite.dependsOn(unidoc in Compile).value, - docusaurusPublishGhpages := docusaurusPublishGhpages.dependsOn(unidoc in Compile).value + (ScalaUnidoc / unidoc / unidocProjectFilter) := inProjects(`zio-s3`), + ScalaUnidoc / unidoc / target := (LocalRootProject / baseDirectory).value / "website" / "static" / "api", + cleanFiles += (ScalaUnidoc / unidoc / target).value, + docusaurusCreateSite := docusaurusCreateSite.dependsOn(Compile / unidoc).value, + docusaurusPublishGhpages := docusaurusPublishGhpages.dependsOn(Compile / unidoc).value ) .dependsOn(`zio-s3`) .enablePlugins(MdocPlugin, DocusaurusPlugin, ScalaUnidocPlugin) diff --git a/docs/quickstart/index.md b/docs/quickstart/index.md index c2253e87..b451a77f 100644 --- a/docs/quickstart/index.md +++ b/docs/quickstart/index.md @@ -55,6 +55,7 @@ import zio.blocking._ import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.s3.model.S3Exception import zio.s3._ +import zio.s3.providers._ // build S3 Layer from basic credentials val s3: Layer[S3Exception, S3] = @@ -62,17 +63,17 @@ val s3: Layer[S3Exception, S3] = // build S3 Layer from System properties or Environment variables val s3: Layer[S3Exception, S3] = - liveM(Region.AF_SOUTH_1, CredentialsProviders.system <> S3Credentials.env) + liveM(Region.AF_SOUTH_1, system <> env) // build S3 Layer from Instance profile credentials val s3: Layer[S3Exception, S3] = - liveM(Region.AF_SOUTH_1, CredentialsProviders.instanceProfile) + liveM(Region.AF_SOUTH_1, instanceProfile) // build S3 Layer from web identity token credentials with STS. awssdk sts module required to be on classpath -val s3: Layer[S3Exception, S3] = liveM(Region.AF_SOUTH_1, CredentialsProviders.webIdentity) +val s3: Layer[S3Exception, S3] = liveM(Region.AF_SOUTH_1, webIdentity) // build S3 Layer from default available credentials providers -val s3: Layer[S3Exception, S3] = liveM(Region.AF_SOUTH_1, CredentialsProviders.default) +val s3: Layer[S3Exception, S3] = liveM(Region.AF_SOUTH_1, default) // use custom logic to fetch aws credentials val zcredentials: ZIO[R, S3Exception, AwsCredentials] = ??? // specific implementation to fetch credentials diff --git a/src/main/scala/zio/s3/package.scala b/src/main/scala/zio/s3/package.scala index f7e13883..92a3dfef 100644 --- a/src/main/scala/zio/s3/package.scala +++ b/src/main/scala/zio/s3/package.scala @@ -26,6 +26,7 @@ import software.amazon.awssdk.services.s3.model.S3Exception import zio.blocking.Blocking import zio.nio.core.file.{ Path => ZPath } import zio.s3.S3Bucket.S3BucketListing +import zio.s3.providers.const import zio.stream.{ Stream, ZStream, ZTransducer } package object s3 { @@ -201,7 +202,7 @@ package object s3 { } def live(region: Region, credentials: AwsCredentials, uriEndpoint: Option[URI] = None): Layer[S3Exception, S3] = - liveM(region, CredentialsProviders.const(credentials.accessKeyId, credentials.secretAccessKey), uriEndpoint) + liveM(region, const(credentials.accessKeyId, credentials.secretAccessKey), uriEndpoint) def liveM[R]( region: Region, @@ -220,7 +221,7 @@ package object s3 { val live: ZLayer[Settings, ConnectionError, S3] = ZLayer.fromFunctionManaged(s => Live.connect( s.get.s3Region, - CredentialsProviders.const(s.get.credentials.accessKeyId, s.get.credentials.secretAccessKey), + const(s.get.credentials.accessKeyId, s.get.credentials.secretAccessKey), None ) ) diff --git a/src/main/scala/zio/s3/providers.scala b/src/main/scala/zio/s3/providers.scala new file mode 100644 index 00000000..ec847fa1 --- /dev/null +++ b/src/main/scala/zio/s3/providers.scala @@ -0,0 +1,88 @@ +package zio.s3 + +import software.amazon.awssdk.auth.credentials.{ + AwsBasicCredentials, + AwsCredentialsProvider, + ContainerCredentialsProvider, + DefaultCredentialsProvider, + EnvironmentVariableCredentialsProvider, + InstanceProfileCredentialsProvider, + ProfileCredentialsProvider, + SystemPropertyCredentialsProvider, + WebIdentityTokenFileCredentialsProvider +} +import zio.blocking.{ Blocking, effectBlocking } +import zio.{ IO, Managed, UManaged, ZIO, ZManaged } + +object providers { + + def const(accessKeyId: String, secretAccessKey: String): UManaged[AwsCredentialsProvider] = + ZManaged.succeedNow[AwsCredentialsProvider](() => AwsBasicCredentials.create(accessKeyId, secretAccessKey)) + + val system: Managed[InvalidCredentials, SystemPropertyCredentialsProvider] = + ZManaged + .succeed(SystemPropertyCredentialsProvider.create()) + .tapM(c => ZIO(c.resolveCredentials())) + .mapError(err => InvalidCredentials(err.getMessage)) + + val env: Managed[InvalidCredentials, EnvironmentVariableCredentialsProvider] = + ZManaged + .succeed(EnvironmentVariableCredentialsProvider.create()) + .tapM(c => + ZIO + .effect(c.resolveCredentials()) + .mapError(err => InvalidCredentials(err.getMessage)) + ) + + val profile: ZManaged[Blocking, InvalidCredentials, ProfileCredentialsProvider] = + ZManaged + .fromAutoCloseable(IO.succeed(ProfileCredentialsProvider.create())) + .tapM(c => + effectBlocking(c.resolveCredentials()) + .mapError(err => InvalidCredentials(err.getMessage)) + ) + + val container: ZManaged[Blocking, InvalidCredentials, ContainerCredentialsProvider] = + ZManaged + .fromAutoCloseable( + IO.succeed( + ContainerCredentialsProvider + .builder() + .build() + ) + ) + .tapM(c => effectBlocking(c.resolveCredentials())) + .mapError(err => InvalidCredentials(err.getMessage)) + + val instanceProfile: ZManaged[Blocking, InvalidCredentials, InstanceProfileCredentialsProvider] = + ZManaged + .fromAutoCloseable( + IO.succeed( + InstanceProfileCredentialsProvider + .create() + ) + ) + .tapM(c => effectBlocking(c.resolveCredentials())) + .mapError(err => InvalidCredentials(err.getMessage)) + + /** + * Use of this layer requires the awssdk sts module to be on the classpath, + * by default zio-s3 required this library + */ + val webIdentity: ZManaged[Blocking, InvalidCredentials, WebIdentityTokenFileCredentialsProvider] = + ZManaged + .succeed( + WebIdentityTokenFileCredentialsProvider + .create() + ) + .tapM(c => effectBlocking(c.resolveCredentials())) + .mapError(err => InvalidCredentials(err.getMessage)) + + /** + * Use default chaining strategy to fetch credentials + */ + val default: ZManaged[Blocking, InvalidCredentials, AwsCredentialsProvider] = + ZManaged.fromAutoCloseable( + IO.succeed(DefaultCredentialsProvider.create()) + ) +} diff --git a/src/main/scala/zio/s3/settings.scala b/src/main/scala/zio/s3/settings.scala index 3e2823fa..dd66bf25 100644 --- a/src/main/scala/zio/s3/settings.scala +++ b/src/main/scala/zio/s3/settings.scala @@ -18,81 +18,7 @@ package zio.s3 import software.amazon.awssdk.auth.credentials._ import software.amazon.awssdk.regions.Region -import zio.blocking.{ Blocking, effectBlocking } -import zio.{ IO, Managed, UManaged, ZIO, ZManaged } - -object CredentialsProviders { - - def const(accessKeyId: String, secretAccessKey: String): UManaged[AwsCredentialsProvider] = - ZManaged.succeedNow[AwsCredentialsProvider](() => AwsBasicCredentials.create(accessKeyId, secretAccessKey)) - - val system: Managed[InvalidCredentials, SystemPropertyCredentialsProvider] = - ZManaged - .succeed(SystemPropertyCredentialsProvider.create()) - .tapM(c => ZIO(c.resolveCredentials())) - .mapError(err => InvalidCredentials(err.getMessage)) - - val env: Managed[InvalidCredentials, EnvironmentVariableCredentialsProvider] = - ZManaged - .succeed(EnvironmentVariableCredentialsProvider.create()) - .tapM(c => - ZIO - .effect(c.resolveCredentials()) - .mapError(err => InvalidCredentials(err.getMessage)) - ) - - val profile: ZManaged[Blocking, InvalidCredentials, ProfileCredentialsProvider] = - ZManaged - .fromAutoCloseable(IO.succeed(ProfileCredentialsProvider.create())) - .tapM(c => - effectBlocking(c.resolveCredentials()) - .mapError(err => InvalidCredentials(err.getMessage)) - ) - - val container: ZManaged[Blocking, InvalidCredentials, ContainerCredentialsProvider] = - ZManaged - .fromAutoCloseable( - IO.succeed( - ContainerCredentialsProvider - .builder() - .build() - ) - ) - .tapM(c => effectBlocking(c.resolveCredentials())) - .mapError(err => InvalidCredentials(err.getMessage)) - - val instanceProfile: ZManaged[Blocking, InvalidCredentials, InstanceProfileCredentialsProvider] = - ZManaged - .fromAutoCloseable( - IO.succeed( - InstanceProfileCredentialsProvider - .create() - ) - ) - .tapM(c => effectBlocking(c.resolveCredentials())) - .mapError(err => InvalidCredentials(err.getMessage)) - - /** - * Use of this layer requires the awssdk sts module to be on the classpath, - * by default zio-s3 required this library - */ - val webIdentity: ZManaged[Blocking, InvalidCredentials, WebIdentityTokenFileCredentialsProvider] = - ZManaged - .succeed( - WebIdentityTokenFileCredentialsProvider - .create() - ) - .tapM(c => effectBlocking(c.resolveCredentials())) - .mapError(err => InvalidCredentials(err.getMessage)) - - /** - * Use default chaining strategy to fetch credentials - */ - val default: ZManaged[Blocking, InvalidCredentials, AwsCredentialsProvider] = - ZManaged.fromAutoCloseable( - IO.succeed(DefaultCredentialsProvider.create()) - ) -} +import zio.{ IO, ZIO } final case class S3Region(region: Region) diff --git a/src/test/scala/zio/s3/S3LayerTest.scala b/src/test/scala/zio/s3/S3LayerTest.scala index dd6e608f..11e43e69 100644 --- a/src/test/scala/zio/s3/S3LayerTest.scala +++ b/src/test/scala/zio/s3/S3LayerTest.scala @@ -12,7 +12,7 @@ object S3LayerSpec extends DefaultRunnableSpec { testM("using ZManaged[R, E, A] in liveM compiles") { assertM( typeCheck( - """liveM(Region.CA_CENTRAL_1, CredentialsProviders.default, Some(URI.create("http://localhost:9000")))""" + """liveM(Region.CA_CENTRAL_1, providers.default, Some(URI.create("http://localhost:9000")))""" ) )(isRight) } diff --git a/src/test/scala/zio/s3/S3ProvidersTest.scala b/src/test/scala/zio/s3/S3ProvidersTest.scala new file mode 100644 index 00000000..e1877b21 --- /dev/null +++ b/src/test/scala/zio/s3/S3ProvidersTest.scala @@ -0,0 +1,93 @@ +package zio.s3 + +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.regions.Region +import zio.s3.providers._ +import zio.test.Assertion._ +import zio.test.TestAspect._ +import zio.test._ +import zio.{ UIO, ZIO } + +object S3ProvidersTest extends DefaultRunnableSpec { + + def setProps(props: (String, String)*): UIO[Unit] = + UIO { + props.foreach { + case (k, v) => + System.setProperty(k, v) + } + } + + def unsetProps(keys: String*): UIO[Unit] = + UIO { + keys.foreach(System.clearProperty) + } + + def spec = + suite("Providers")( + testM("cred with const") { + assertM(const("k", "v").useNow.map(_.resolveCredentials()))( + equalTo(AwsBasicCredentials.create("k", "v")) + ) + }, + testM("cred with default fallback const") { + assertM( + (env <> const("k", "v")).useNow.map(_.resolveCredentials()) + )(equalTo(AwsBasicCredentials.create("k", "v"))) + }, + testM("cred in system properties") { + for { + cred <- system.use(p => ZIO(p.resolveCredentials())) + } yield assert(cred)(equalTo(AwsBasicCredentials.create("k1", "s1"))) + } @@ flaky @@ around_( + setProps(("aws.accessKeyId", "k1"), ("aws.secretAccessKey", "s1")), + unsetProps("aws.accessKeyId", "aws.secretAccessKey") + ), + testM("no cred in system properties") { + for { + failure <- system.useNow.flip.map(_.getMessage) + } yield assert(failure)(isNonEmptyString) + } @@ around_( + unsetProps("aws.accessKeyId", "aws.secretAccessKey"), + UIO.unit + ), + testM("no cred in environment properties") { + for { + failure <- env.useNow.flip.map(_.getMessage) + } yield assert(failure)(isNonEmptyString) + }, + testM("no cred in profile") { + for { + failure <- profile.useNow.flip.map(_.getMessage) + } yield assert(failure)(isNonEmptyString) + }, + testM("no cred in container") { + for { + failure <- container.useNow.flip.map(_.getMessage) + } yield assert(failure)(isNonEmptyString) + }, + testM("no cred in instance profile credentials") { + for { + failure <- instanceProfile.useNow.flip.map(_.getMessage) + } yield assert(failure)(isNonEmptyString) + }, + testM("no cred in webidentity credentials") { + for { + failure <- webIdentity.useNow.flip.map(_.getMessage) + } yield assert(failure)(isNonEmptyString) + }, + testM("settings from invalid creds") { + for { + failure <- settings( + Region.AF_SOUTH_1, + system.useNow.map(_.resolveCredentials()) + ).build.useNow.flip + } yield assert(failure.getMessage)(isNonEmptyString) + }, + testM("no cred when chain all providers") { + for { + failure <- default.use(c => ZIO.effect(c.resolveCredentials())).flip.map(_.getMessage) + } yield assert(failure)(isNonEmptyString) + } + ) @@ sequential +} diff --git a/src/test/scala/zio/s3/S3SettingsTest.scala b/src/test/scala/zio/s3/S3SettingsTest.scala index 99ae0d35..d6a0a94d 100644 --- a/src/test/scala/zio/s3/S3SettingsTest.scala +++ b/src/test/scala/zio/s3/S3SettingsTest.scala @@ -3,112 +3,28 @@ package zio.s3 import software.amazon.awssdk.auth.credentials.AwsBasicCredentials import software.amazon.awssdk.regions.Region import zio.test.Assertion._ -import zio.test.TestAspect._ import zio.test._ -import zio.{ UIO, ZIO } object S3SettingsTest extends DefaultRunnableSpec { - def setProps(props: (String, String)*): UIO[Unit] = - UIO { - props.foreach { - case (k, v) => - System.setProperty(k, v) - } - } - - def unsetProps(keys: String*): UIO[Unit] = - UIO { - keys.foreach(System.clearProperty) - } - def spec = suite("Settings")( - suite("Regions")( - testM("invalid region") { - for { - failure <- S3Settings - .from(Region.of("invalid"), AwsBasicCredentials.create("key", "secret")) - .foldCause(_.failureOption.map(_.message).mkString, _ => "") - } yield assert(failure)(equalTo("Invalid aws region provided : invalid")) - }, - testM("valid region") { - for { - success <- S3Settings.from(Region.US_EAST_2, AwsBasicCredentials.create("key", "secret")) - } yield assert(success.s3Region.region -> success.credentials)( - equalTo(Region.US_EAST_2 -> AwsBasicCredentials.create("key", "secret")) - ) - }, - test("unsafe Region") { - assert(S3Region.unsafeFromString("blah").region)(equalTo(Region.of("blah"))) - } - ), - suite("credentials")( - testM("cred with const") { - assertM(CredentialsProviders.const("k", "v").useNow.map(_.resolveCredentials()))( - equalTo(AwsBasicCredentials.create("k", "v")) - ) - }, - testM("cred with default fallback const") { - assertM( - (CredentialsProviders.env <> CredentialsProviders.const("k", "v")).useNow.map(_.resolveCredentials()) - )(equalTo(AwsBasicCredentials.create("k", "v"))) - }, - testM("cred in system properties") { - for { - cred <- CredentialsProviders.system.use(p => ZIO(p.resolveCredentials())) - } yield assert(cred)(equalTo(AwsBasicCredentials.create("k1", "s1"))) - } @@ flaky @@ around_( - setProps(("aws.accessKeyId", "k1"), ("aws.secretAccessKey", "s1")), - unsetProps("aws.accessKeyId", "aws.secretAccessKey") - ), - testM("no cred in system properties") { - for { - failure <- CredentialsProviders.system.useNow.flip.map(_.getMessage) - } yield assert(failure)(isNonEmptyString) - } @@ around_( - unsetProps("aws.accessKeyId", "aws.secretAccessKey"), - UIO.unit - ), - testM("no cred in environment properties") { - for { - failure <- CredentialsProviders.env.useNow.flip.map(_.getMessage) - } yield assert(failure)(isNonEmptyString) - }, - testM("no cred in profile") { - for { - failure <- CredentialsProviders.profile.useNow.flip.map(_.getMessage) - } yield assert(failure)(isNonEmptyString) - }, - testM("no cred in container") { - for { - failure <- CredentialsProviders.container.useNow.flip.map(_.getMessage) - } yield assert(failure)(isNonEmptyString) - }, - testM("no cred in instance profile credentials") { - for { - failure <- CredentialsProviders.instanceProfile.useNow.flip.map(_.getMessage) - } yield assert(failure)(isNonEmptyString) - }, - testM("no cred in webidentity credentials") { - for { - failure <- CredentialsProviders.webIdentity.useNow.flip.map(_.getMessage) - } yield assert(failure)(isNonEmptyString) - }, - testM("settings from invalid creds") { - for { - failure <- settings( - Region.AF_SOUTH_1, - CredentialsProviders.system.useNow.map(_.resolveCredentials()) - ).build.useNow.flip - } yield assert(failure.getMessage)(isNonEmptyString) - }, - testM("no cred when chain all providers") { - for { - failure <- CredentialsProviders.default.use(c => ZIO.effect(c.resolveCredentials())).flip.map(_.getMessage) - } yield assert(failure)(isNonEmptyString) - } - ) @@ sequential + testM("invalid region") { + for { + failure <- S3Settings + .from(Region.of("invalid"), AwsBasicCredentials.create("key", "secret")) + .foldCause(_.failureOption.map(_.message).mkString, _ => "") + } yield assert(failure)(equalTo("Invalid aws region provided : invalid")) + }, + testM("valid region") { + for { + success <- S3Settings.from(Region.US_EAST_2, AwsBasicCredentials.create("key", "secret")) + } yield assert(success.s3Region.region -> success.credentials)( + equalTo(Region.US_EAST_2 -> AwsBasicCredentials.create("key", "secret")) + ) + }, + test("unsafe Region") { + assert(S3Region.unsafeFromString("blah").region)(equalTo(Region.of("blah"))) + } ) - }