From 3a0b7faac934607634dad1160ffb584e267a9be6 Mon Sep 17 00:00:00 2001 From: Hossein Naderi Date: Mon, 1 May 2023 15:19:45 +0330 Subject: [PATCH] Support for kubeconfig for creating clients Added separate http4s backends Added java ssl module --- .github/workflows/ci.yml | 4 +- build.sbt | 60 +++++- example/src/main/scala/Http4sExample.scala | 16 +- .../main/scala/BlazeKubernetesClient.scala | 36 ++++ .../src/main/scala/PlatformCompanion.scala | 19 ++ .../src/main/scala/PlatformCompanion.scala | 34 ++++ .../src/main/scala/PlatformCompanion.scala | 19 ++ .../main/scala/EmberKubernetesClient.scala | 32 ++++ .../main/scala/NettyKubernetesClient.scala | 36 ++++ .../.jvm/src/main/scala/JVMPlatform.scala | 134 ++++++++++++++ .../http4s/src/main/scala/Http4sBackend.scala | 14 +- .../main/scala/Http4sKubernetesClient.scala | 8 +- .../src/main/scala-2.12/Conversions.scala | 21 +++ .../src/main/scala-2.13/Conversions.scala | 21 +++ .../src/main/scala-3/Conversions.scala | 21 +++ .../java-ssl/src/main/scala/SSLContexts.scala | 172 ++++++++++++++++++ 16 files changed, 614 insertions(+), 33 deletions(-) create mode 100644 modules/http4s-blaze/src/main/scala/BlazeKubernetesClient.scala create mode 100644 modules/http4s-ember/.js/src/main/scala/PlatformCompanion.scala create mode 100644 modules/http4s-ember/.jvm/src/main/scala/PlatformCompanion.scala create mode 100644 modules/http4s-ember/.native/src/main/scala/PlatformCompanion.scala create mode 100644 modules/http4s-ember/src/main/scala/EmberKubernetesClient.scala create mode 100644 modules/http4s-netty/src/main/scala/NettyKubernetesClient.scala create mode 100644 modules/http4s/.jvm/src/main/scala/JVMPlatform.scala create mode 100644 modules/java-ssl/src/main/scala-2.12/Conversions.scala create mode 100644 modules/java-ssl/src/main/scala-2.13/Conversions.scala create mode 100644 modules/java-ssl/src/main/scala-3/Conversions.scala create mode 100644 modules/java-ssl/src/main/scala/SSLContexts.scala diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8b4d1c8..63eab948 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,11 +120,11 @@ jobs: - name: Make target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: mkdir -p modules/json4s/.js/target modules/http4s/.jvm/target modules/http4s/.js/target modules/client-test/.js/target modules/circe/.jvm/target modules/scalacheck/.native/target target modules/client/.native/target modules/client-test/.jvm/target unidocs/target modules/sttp/.native/target .js/target site/target modules/client/.js/target modules/json4s/.native/target modules/jawn/.jvm/target modules/codec-test/.jvm/target modules/objects-test/.js/target modules/client/.jvm/target modules/codec-test/.native/target modules/manifests/.js/target modules/circe/.js/target modules/objects/.js/target modules/objects/.jvm/target modules/jawn/.js/target modules/spray-json/.jvm/target modules/play-json/.jvm/target modules/zio/.jvm/target modules/codec-test/.js/target modules/manifests/.jvm/target .jvm/target .native/target modules/sttp/.js/target modules/objects-test/.jvm/target modules/http4s/.native/target modules/sttp/.jvm/target modules/jawn/.native/target modules/circe/.native/target example/.jvm/target modules/zio-json/.js/target modules/objects/.native/target modules/json4s/.jvm/target modules/scalacheck/.jvm/target modules/zio-json/.jvm/target modules/manifests/.native/target modules/scalacheck/.js/target project/target + run: mkdir -p modules/http4s-ember/.jvm/target modules/json4s/.js/target modules/http4s-ember/.js/target modules/http4s/.jvm/target modules/http4s/.js/target modules/client-test/.js/target modules/circe/.jvm/target modules/scalacheck/.native/target target modules/client/.native/target modules/client-test/.jvm/target unidocs/target modules/sttp/.native/target .js/target site/target modules/client/.js/target modules/json4s/.native/target modules/jawn/.jvm/target modules/codec-test/.jvm/target modules/http4s-netty/.jvm/target modules/objects-test/.js/target modules/client/.jvm/target modules/codec-test/.native/target modules/manifests/.js/target modules/circe/.js/target modules/objects/.js/target modules/objects/.jvm/target modules/jawn/.js/target modules/spray-json/.jvm/target modules/play-json/.jvm/target modules/zio/.jvm/target modules/codec-test/.js/target modules/manifests/.jvm/target modules/http4s-blaze/.jvm/target .jvm/target .native/target modules/sttp/.js/target modules/objects-test/.jvm/target modules/http4s/.native/target modules/sttp/.jvm/target modules/jawn/.native/target modules/circe/.native/target example/.jvm/target modules/zio-json/.js/target modules/objects/.native/target modules/json4s/.jvm/target modules/scalacheck/.jvm/target modules/zio-json/.jvm/target modules/java-ssl/.jvm/target modules/http4s-ember/.native/target modules/manifests/.native/target modules/scalacheck/.js/target project/target - name: Compress target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') - run: tar cf targets.tar modules/json4s/.js/target modules/http4s/.jvm/target modules/http4s/.js/target modules/client-test/.js/target modules/circe/.jvm/target modules/scalacheck/.native/target target modules/client/.native/target modules/client-test/.jvm/target unidocs/target modules/sttp/.native/target .js/target site/target modules/client/.js/target modules/json4s/.native/target modules/jawn/.jvm/target modules/codec-test/.jvm/target modules/objects-test/.js/target modules/client/.jvm/target modules/codec-test/.native/target modules/manifests/.js/target modules/circe/.js/target modules/objects/.js/target modules/objects/.jvm/target modules/jawn/.js/target modules/spray-json/.jvm/target modules/play-json/.jvm/target modules/zio/.jvm/target modules/codec-test/.js/target modules/manifests/.jvm/target .jvm/target .native/target modules/sttp/.js/target modules/objects-test/.jvm/target modules/http4s/.native/target modules/sttp/.jvm/target modules/jawn/.native/target modules/circe/.native/target example/.jvm/target modules/zio-json/.js/target modules/objects/.native/target modules/json4s/.jvm/target modules/scalacheck/.jvm/target modules/zio-json/.jvm/target modules/manifests/.native/target modules/scalacheck/.js/target project/target + run: tar cf targets.tar modules/http4s-ember/.jvm/target modules/json4s/.js/target modules/http4s-ember/.js/target modules/http4s/.jvm/target modules/http4s/.js/target modules/client-test/.js/target modules/circe/.jvm/target modules/scalacheck/.native/target target modules/client/.native/target modules/client-test/.jvm/target unidocs/target modules/sttp/.native/target .js/target site/target modules/client/.js/target modules/json4s/.native/target modules/jawn/.jvm/target modules/codec-test/.jvm/target modules/http4s-netty/.jvm/target modules/objects-test/.js/target modules/client/.jvm/target modules/codec-test/.native/target modules/manifests/.js/target modules/circe/.js/target modules/objects/.js/target modules/objects/.jvm/target modules/jawn/.js/target modules/spray-json/.jvm/target modules/play-json/.jvm/target modules/zio/.jvm/target modules/codec-test/.js/target modules/manifests/.jvm/target modules/http4s-blaze/.jvm/target .jvm/target .native/target modules/sttp/.js/target modules/objects-test/.jvm/target modules/http4s/.native/target modules/sttp/.jvm/target modules/jawn/.native/target modules/circe/.native/target example/.jvm/target modules/zio-json/.js/target modules/objects/.native/target modules/json4s/.jvm/target modules/scalacheck/.jvm/target modules/zio-json/.jvm/target modules/java-ssl/.jvm/target modules/http4s-ember/.native/target modules/manifests/.native/target modules/scalacheck/.js/target project/target - name: Upload target directories if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') diff --git a/build.sbt b/build.sbt index b97dfe0f..cf9814cb 100644 --- a/build.sbt +++ b/build.sbt @@ -41,7 +41,11 @@ lazy val root = objectsTest, clientTest, client, + javaSSL, http4s, + http4sEmber, + http4sNetty, + http4sBlaze, zio, sttp, codecTest, @@ -96,24 +100,68 @@ lazy val client = module("client") { description := "client core for kubernetes", k8sUnmanagedTarget := rootDir.value / "modules" / "client" / "src" / "main" / "scala" ) - .jvmSettings( - libraryDependencies += "org.bouncycastle" % "bcpkix-jdk18on" % "1.73" - ) - .dependsOn(objects) + .dependsOn(objects, manifests) .enablePlugins(KubernetesJsonPointerGeneratorPlugin) } +lazy val javaSSL = module("java-ssl") { + crossProject(JVMPlatform) + .crossType(CrossType.Pure) + .settings( + description := "java ssl for kubernetes config", + libraryDependencies ++= Seq( + "org.bouncycastle" % "bcpkix-jdk18on" % "1.73" + ) + ) + .dependsOn(client) +} + lazy val http4s = module("http4s") { crossProject(JVMPlatform, JSPlatform, NativePlatform) .crossType(CrossType.Pure) .settings( description := "http4s based client for kubernetes", libraryDependencies ++= Seq( - "org.http4s" %%% "http4s-ember-client" % "0.23.18", + "org.http4s" %%% "http4s-client" % "0.23.18", "org.typelevel" %%% "jawn-fs2" % "2.4.0" ) ) .dependsOn(client, jawn) + .jvmConfigure(_.dependsOn(javaSSL.jvm)) +} + +lazy val http4sEmber = module("http4s-ember") { + crossProject(JVMPlatform, JSPlatform, NativePlatform) + .crossType(CrossType.Pure) + .settings( + description := "http4s ember based client for kubernetes", + libraryDependencies ++= Seq( + "org.http4s" %%% "http4s-ember-client" % "0.23.18" + ) + ) + .dependsOn(http4s) +} +lazy val http4sNetty = module("http4s-netty") { + crossProject(JVMPlatform) + .crossType(CrossType.Pure) + .settings( + description := "http4s netty based client for kubernetes", + libraryDependencies ++= Seq( + "org.http4s" %% "http4s-netty-client" % "0.5.6" + ) + ) + .dependsOn(http4s) +} +lazy val http4sBlaze = module("http4s-blaze") { + crossProject(JVMPlatform) + .crossType(CrossType.Pure) + .settings( + description := "http4s blaze based client for kubernetes", + libraryDependencies ++= Seq( + "org.http4s" %% "http4s-blaze-client" % "0.23.14" + ) + ) + .dependsOn(http4s) } lazy val sttp = module("sttp") { @@ -345,7 +393,7 @@ lazy val example = crossProject(JVMPlatform) "com.softwaremill.sttp.client3" %%% "circe" % "3.8.15" ) ) - .dependsOn(http4s, circe, zio, sttp) + .dependsOn(http4sNetty, http4sEmber, circe, zio, sttp) .enablePlugins(NoPublishPlugin) def addAlias(name: String)(tasks: String*) = diff --git a/example/src/main/scala/Http4sExample.scala b/example/src/main/scala/Http4sExample.scala index 8ac76dcb..3b5660ff 100644 --- a/example/src/main/scala/Http4sExample.scala +++ b/example/src/main/scala/Http4sExample.scala @@ -20,19 +20,18 @@ import cats.effect._ import cats.implicits._ import dev.hnaderi.k8s.circe._ import dev.hnaderi.k8s.client._ -import dev.hnaderi.k8s.implicits._ +import dev.hnaderi.k8s.client.http4s.EmberKubernetesClient import dev.hnaderi.k8s.client.implicits._ +import dev.hnaderi.k8s.implicits._ import fs2.Stream._ import io.circe.Json import io.k8s.api.core.v1.ConfigMap import io.k8s.apimachinery.pkg.apis.meta.v1.ObjectMeta import org.http4s.circe._ -//NOTE run `kubectl proxy` before running this example object Http4sExample extends IOApp { - private val client = - Http4sKubernetesClient.fromUrl[IO, Json]("http://localhost:8001") + private val client = EmberKubernetesClient.defaultConfig[IO, Json] def watchNodes(cl: StreamingClient[fs2.Stream[IO, *]]) = CoreV1.nodes @@ -57,8 +56,9 @@ object Http4sExample extends IOApp { ) def operations(cl: HttpClient[IO]) = for { + _ <- APIs.namespace("hnaderi").configmaps.list.send(cl).flatMap(IO.println) _ <- APIs - .namespace("default") + .namespace("hnaderi") .configmaps .create( ConfigMap( @@ -67,14 +67,14 @@ object Http4sExample extends IOApp { ) ) .send(cl) - a <- APIs.namespace("default").configmaps.get("example").send(cl) + a <- APIs.namespace("hnaderi").configmaps.get("example").send(cl) b <- APIs - .namespace("default") + .namespace("hnaderi") .configmaps .replace("example", a.withData(Map("test2" -> "value2"))) .send(cl) _ <- IO.println(b) - _ <- APIs.namespace("default").configmaps.delete("example").send(cl) + _ <- APIs.namespace("hnaderi").configmaps.delete("example").send(cl) } yield () def operations2(cl: HttpClient[IO]) = for { diff --git a/modules/http4s-blaze/src/main/scala/BlazeKubernetesClient.scala b/modules/http4s-blaze/src/main/scala/BlazeKubernetesClient.scala new file mode 100644 index 00000000..b7c18c9c --- /dev/null +++ b/modules/http4s-blaze/src/main/scala/BlazeKubernetesClient.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Hossein Naderi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.hnaderi.k8s.client +package http4s + +import cats.effect.kernel.Async +import cats.effect.kernel.Resource +import org.http4s.blaze.client.BlazeClientBuilder +import org.http4s.client.Client + +import javax.net.ssl.SSLContext + +object BlazeKubernetesClient extends Http4sKubernetesClient with JVMPlatform { + + override protected def buildClient[F[_]: Async]: Resource[F, Client[F]] = + BlazeClientBuilder[F].resource + + override protected def buildWithSSLContext[F[_]: Async] + : SSLContext => Resource[F, Client[F]] = + BlazeClientBuilder[F].withSslContext(_).resource + +} diff --git a/modules/http4s-ember/.js/src/main/scala/PlatformCompanion.scala b/modules/http4s-ember/.js/src/main/scala/PlatformCompanion.scala new file mode 100644 index 00000000..2027051a --- /dev/null +++ b/modules/http4s-ember/.js/src/main/scala/PlatformCompanion.scala @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Hossein Naderi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.hnaderi.k8s.client + +trait PlatformCompanion {} diff --git a/modules/http4s-ember/.jvm/src/main/scala/PlatformCompanion.scala b/modules/http4s-ember/.jvm/src/main/scala/PlatformCompanion.scala new file mode 100644 index 00000000..3d5ba9db --- /dev/null +++ b/modules/http4s-ember/.jvm/src/main/scala/PlatformCompanion.scala @@ -0,0 +1,34 @@ +/* + * Copyright 2022 Hossein Naderi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.hnaderi.k8s.client + +import cats.effect.kernel.Async +import cats.effect.kernel.Resource +import fs2.io.net.tls.TLSContext +import org.http4s.client.Client +import org.http4s.ember.client.EmberClientBuilder + +import javax.net.ssl.SSLContext + +trait PlatformCompanion extends JVMPlatform { self: Http4sKubernetesClient => + override protected def buildWithSSLContext[F[_]: Async] + : SSLContext => Resource[F, Client[F]] = ctx => + EmberClientBuilder + .default[F] + .withTLSContext(TLSContext.Builder.forAsync[F].fromSSLContext(ctx)) + .build +} diff --git a/modules/http4s-ember/.native/src/main/scala/PlatformCompanion.scala b/modules/http4s-ember/.native/src/main/scala/PlatformCompanion.scala new file mode 100644 index 00000000..2027051a --- /dev/null +++ b/modules/http4s-ember/.native/src/main/scala/PlatformCompanion.scala @@ -0,0 +1,19 @@ +/* + * Copyright 2022 Hossein Naderi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.hnaderi.k8s.client + +trait PlatformCompanion {} diff --git a/modules/http4s-ember/src/main/scala/EmberKubernetesClient.scala b/modules/http4s-ember/src/main/scala/EmberKubernetesClient.scala new file mode 100644 index 00000000..b914a319 --- /dev/null +++ b/modules/http4s-ember/src/main/scala/EmberKubernetesClient.scala @@ -0,0 +1,32 @@ +/* + * Copyright 2022 Hossein Naderi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.hnaderi.k8s.client +package http4s + +import cats.effect.kernel.Async +import cats.effect.kernel.Resource +import org.http4s.client.Client +import org.http4s.ember.client.EmberClientBuilder + +object EmberKubernetesClient + extends Http4sKubernetesClient + with PlatformCompanion { + + override protected def buildClient[F[_]: Async]: Resource[F, Client[F]] = + EmberClientBuilder.default[F].build + +} diff --git a/modules/http4s-netty/src/main/scala/NettyKubernetesClient.scala b/modules/http4s-netty/src/main/scala/NettyKubernetesClient.scala new file mode 100644 index 00000000..31f4c334 --- /dev/null +++ b/modules/http4s-netty/src/main/scala/NettyKubernetesClient.scala @@ -0,0 +1,36 @@ +/* + * Copyright 2022 Hossein Naderi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.hnaderi.k8s.client +package http4s + +import cats.effect.kernel.Async +import cats.effect.kernel.Resource +import org.http4s.client.Client +import org.http4s.netty.client.NettyClientBuilder + +import javax.net.ssl.SSLContext + +object NettyKubernetesClient extends Http4sKubernetesClient with JVMPlatform { + + override protected def buildClient[F[_]: Async]: Resource[F, Client[F]] = + NettyClientBuilder[F].resource + + override protected def buildWithSSLContext[F[_]: Async] + : SSLContext => Resource[F, Client[F]] = + NettyClientBuilder[F].withSSLContext(_).resource + +} diff --git a/modules/http4s/.jvm/src/main/scala/JVMPlatform.scala b/modules/http4s/.jvm/src/main/scala/JVMPlatform.scala new file mode 100644 index 00000000..bcb103da --- /dev/null +++ b/modules/http4s/.jvm/src/main/scala/JVMPlatform.scala @@ -0,0 +1,134 @@ +/* + * Copyright 2022 Hossein Naderi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.hnaderi.k8s.client + +import cats.effect.kernel.Async +import cats.effect.kernel.Resource +import cats.effect.std.Env +import cats.syntax.all._ +import dev.hnaderi.k8s.manifest +import dev.hnaderi.k8s.utils._ +import fs2.io.file.Files +import fs2.io.file.Path +import org.http4s._ +import org.http4s.client.Client + +import java.io.FileNotFoundException +import javax.net.ssl.SSLContext + +private[client] trait JVMPlatform { self: Http4sKubernetesClient => + protected def buildWithSSLContext[F[_]: Async] + : SSLContext => Resource[F, Client[F]] + + def fromConfig[F[_], T]( + config: Config, + context: Option[String] = None + )(implicit + F: Async[F], + enc: EntityEncoder[F, T], + dec: EntityDecoder[F, T], + builder: Builder[T], + reader: Reader[T] + ): Resource[F, KClient[F]] = { + val currentContext = context.getOrElse(config.`current-context`) + val toConnect = for { + ctx <- config.contexts.find(_.name == currentContext) + cluster <- config.clusters.find(_.name == ctx.context.cluster) + user <- config.users.find(_.name == ctx.context.user) + } yield (cluster.cluster, cluster.cluster.server, user.user) + + toConnect match { + case None => + Resource.eval( + new IllegalArgumentException( + "Cannot find where/how to connect using the provided config!" + ).raiseError + ) + case Some((cluster, server, auth)) => + val sslContext = F.blocking(SSLContexts.from(cluster, auth)) + + Resource + .eval(sslContext) + .flatMap(buildWithSSLContext) + .map(Http4sBackend.fromClient(_)) + .map(HttpClient.streaming(server, _, AuthenticationParams.from(auth))) + } + + } + + def fromPath[F[_], T]( + config: Path, + context: Option[String] = None + )(implicit + F: Async[F], + enc: EntityEncoder[F, T], + dec: EntityDecoder[F, T], + builder: Builder[T], + reader: Reader[T] + ): Resource[F, KClient[F]] = for { + str <- Resource.eval(Files[F].readUtf8(config).compile.string) + conf <- Resource.eval(F.fromEither(manifest.parse[Config](str))) + client <- fromConfig(conf, context) + } yield client + + def fromFile[F[_], T]( + config: String, + context: Option[String] = None + )(implicit + F: Async[F], + enc: EntityEncoder[F, T], + dec: EntityDecoder[F, T], + builder: Builder[T], + reader: Reader[T] + ): Resource[F, KClient[F]] = fromPath(Path(config), context) + + def defaultConfig[F[_], T](implicit + F: Async[F], + env: Env[F], + files: Files[F], + enc: EntityEncoder[F, T], + dec: EntityDecoder[F, T], + builder: Builder[T], + reader: Reader[T] + ): Resource[F, KClient[F]] = { + val homeConfig = System.getProperty("user.home") match { + case null => Path("~") / ".kube" / "config" + case value => Path(value) / ".kube" / "config" + } + val envConfig = env.get("KUBECONFIG").map(_.map(Path(_))) + + val fromEnv: F[Option[Path]] = envConfig.flatMap { + case None => F.pure(None) + case Some(value) => files.exists(value).ifF(Some(value), None) + } + + val findFile = envConfig.flatMap { + case Some(value) => value.pure + case None => + files + .exists(homeConfig) + .ifM( + homeConfig.pure, + F.raiseError( + new FileNotFoundException("Cannot find kubeconfig file!") + ) + ) + } + + Resource.eval(findFile).flatMap(fromPath(_)) + } +} diff --git a/modules/http4s/src/main/scala/Http4sBackend.scala b/modules/http4s/src/main/scala/Http4sBackend.scala index 29f9fc92..3269c895 100644 --- a/modules/http4s/src/main/scala/Http4sBackend.scala +++ b/modules/http4s/src/main/scala/Http4sBackend.scala @@ -17,9 +17,7 @@ package dev.hnaderi.k8s.client import cats.effect.Concurrent -import cats.effect.kernel.Async -import cats.effect.kernel.Resource -import cats.implicits._ +import cats.syntax.all._ import dev.hnaderi.k8s.utils._ import fs2.Stream import io.k8s.apimachinery.pkg.apis.meta.v1 @@ -170,14 +168,4 @@ object Http4sBackend { builder: Builder[T], reader: Reader[T] ): Http4sBackend[F, T] = new Http4sBackend[F, T](client) - - def fromUrl[F[_], T](implicit - F: Async[F], - enc: EntityEncoder[F, T], - dec: EntityDecoder[F, T], - builder: Builder[T], - reader: Reader[T] - ): Resource[F, Http4sBackend[F, T]] = - http4s.ember.client.EmberClientBuilder.default[F].build.map(fromClient(_)) - } diff --git a/modules/http4s/src/main/scala/Http4sKubernetesClient.scala b/modules/http4s/src/main/scala/Http4sKubernetesClient.scala index e7da275a..25f275f2 100644 --- a/modules/http4s/src/main/scala/Http4sKubernetesClient.scala +++ b/modules/http4s/src/main/scala/Http4sKubernetesClient.scala @@ -23,11 +23,12 @@ import dev.hnaderi.k8s.utils._ import fs2.Stream import org.http4s._ import org.http4s.client.Client -import org.http4s.ember.client.EmberClientBuilder -object Http4sKubernetesClient { +trait Http4sKubernetesClient { type KClient[F[_]] = HttpClient[F] with StreamingClient[Stream[F, *]] + protected def buildClient[F[_]: Async]: Resource[F, Client[F]] + def fromClient[F[_], T]( baseUrl: String, client: Client[F] @@ -48,6 +49,5 @@ object Http4sKubernetesClient { dec: EntityDecoder[F, T], builder: Builder[T], reader: Reader[T] - ): Resource[F, KClient[F]] = - EmberClientBuilder.default[F].build.map(fromClient(baseUrl, _)) + ): Resource[F, KClient[F]] = buildClient[F].map(fromClient(baseUrl, _)) } diff --git a/modules/java-ssl/src/main/scala-2.12/Conversions.scala b/modules/java-ssl/src/main/scala-2.12/Conversions.scala new file mode 100644 index 00000000..5df9b884 --- /dev/null +++ b/modules/java-ssl/src/main/scala-2.12/Conversions.scala @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Hossein Naderi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.hnaderi.k8s.client + +import scala.collection.convert._ + +private object Conversions extends DecorateAsScala with DecorateAsJava diff --git a/modules/java-ssl/src/main/scala-2.13/Conversions.scala b/modules/java-ssl/src/main/scala-2.13/Conversions.scala new file mode 100644 index 00000000..f61eff38 --- /dev/null +++ b/modules/java-ssl/src/main/scala-2.13/Conversions.scala @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Hossein Naderi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.hnaderi.k8s.client + +import scala.collection.convert._ + +private object Conversions extends AsScalaExtensions with AsJavaExtensions diff --git a/modules/java-ssl/src/main/scala-3/Conversions.scala b/modules/java-ssl/src/main/scala-3/Conversions.scala new file mode 100644 index 00000000..d12e9cbc --- /dev/null +++ b/modules/java-ssl/src/main/scala-3/Conversions.scala @@ -0,0 +1,21 @@ +/* + * Copyright 2022 Hossein Naderi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.hnaderi.k8s.client + +import scala.collection.convert.* + +private object Conversions extends AsScalaExtensions with AsJavaExtensions diff --git a/modules/java-ssl/src/main/scala/SSLContexts.scala b/modules/java-ssl/src/main/scala/SSLContexts.scala new file mode 100644 index 00000000..92f798a7 --- /dev/null +++ b/modules/java-ssl/src/main/scala/SSLContexts.scala @@ -0,0 +1,172 @@ +/* + * Copyright 2022 Hossein Naderi + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dev.hnaderi.k8s +package client + +import org.bouncycastle.jce.provider.BouncyCastleProvider +import org.bouncycastle.openssl.PEMKeyPair +import org.bouncycastle.openssl.PEMParser +import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter + +import java.io.ByteArrayInputStream +import java.io.File +import java.io.FileInputStream +import java.io.InputStreamReader +import java.security.KeyStore +import java.security.SecureRandom +import java.security.Security +import java.security.cert.CertificateFactory +import java.security.cert.X509Certificate +import java.util.Base64 +import javax.net.ssl._ + +import Conversions._ + +private[client] object SSLContexts { + private val TrustStoreSystemProperty = "javax.net.ssl.trustStore" + private val TrustStorePasswordSystemProperty = + "javax.net.ssl.trustStorePassword" + private val KeyStoreSystemProperty = "javax.net.ssl.keyStore" + private val KeyStorePasswordSystemProperty = "javax.net.ssl.keyStorePassword" + + def from( + cluster: Cluster, + auth: AuthInfo, + clientKeyPassword: Option[String] = None + ): SSLContext = { + val sslContext = SSLContext.getInstance("TLS") + sslContext.init( + keyManagers(auth, clientKeyPassword), + trustManagers(cluster), + new SecureRandom() + ) + + sslContext + } + + private def trustManagers(config: Cluster) = { + val certDataStream = config.`certificate-authority-data`.map(data => + new ByteArrayInputStream(Base64.getDecoder.decode(data)) + ) + val certFileStream = + config.`certificate-authority`.map(new FileInputStream(_)) + + certDataStream.orElse(certFileStream).foreach { certStream => + val certificateFactory = CertificateFactory.getInstance("X509") + val certificates = + certificateFactory.generateCertificates(certStream).asScala + certificates + .map(_.asInstanceOf[X509Certificate]) + .zipWithIndex + .foreach { case (certificate, i) => + val alias = s"${certificate.getSubjectX500Principal.getName}-$i" + defaultTrustStore.setCertificateEntry(alias, certificate) + } + } + + val trustManagerFactory = + TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm) + trustManagerFactory.init(defaultTrustStore) + trustManagerFactory.getTrustManagers + } + + private lazy val defaultTrustStore = { + val securityDirectory = s"${System.getProperty("java.home")}/lib/security" + + val propertyTrustStoreFile = + Option(System.getProperty(TrustStoreSystemProperty, "")) + .filter(_.nonEmpty) + .map(new File(_)) + val jssecacertsFile = + Option(new File(s"$securityDirectory/jssecacerts")).filter(f => + f.exists && f.isFile + ) + val cacertsFile = new File(s"$securityDirectory/cacerts") + + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) + keyStore.load( + new FileInputStream( + propertyTrustStoreFile.orElse(jssecacertsFile).getOrElse(cacertsFile) + ), + System + .getProperty(TrustStorePasswordSystemProperty, "changeit") + .toCharArray + ) + keyStore + } + + private def keyManagers( + config: AuthInfo, + clientKeyPass: Option[String] = None + ) = { + // Client certificate + val certDataStream = config.`client-certificate-data`.map(data => + new ByteArrayInputStream(Base64.getDecoder.decode(data)) + ) + val certFileStream = config.`client-certificate`.map(new FileInputStream(_)) + + // Client key + val keyDataStream = config.`client-key-data`.map(data => + new ByteArrayInputStream(Base64.getDecoder.decode(data)) + ) + val keyFileStream = config.`client-key`.map(new FileInputStream(_)) + + for { + keyStream <- keyDataStream.orElse(keyFileStream) + certStream <- certDataStream.orElse(certFileStream) + } yield { + Security.addProvider(new BouncyCastleProvider()) + val pemKeyPair = new PEMParser(new InputStreamReader(keyStream)) + .readObject() + .asInstanceOf[PEMKeyPair] // scalafix:ok + val privateKey = new JcaPEMKeyConverter() + .setProvider("BC") + .getPrivateKey(pemKeyPair.getPrivateKeyInfo) + + val certificateFactory = CertificateFactory.getInstance("X509") + val certificate = certificateFactory + .generateCertificate(certStream) + .asInstanceOf[X509Certificate] + + defaultKeyStore.setKeyEntry( + certificate.getSubjectX500Principal.getName, + privateKey, + clientKeyPass.fold(Array.empty[Char])(_.toCharArray), + Array(certificate) + ) + } + + val keyManagerFactory = + KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm) + keyManagerFactory.init(defaultKeyStore, Array.empty) + keyManagerFactory.getKeyManagers + } + + private lazy val defaultKeyStore = { + val propertyKeyStoreFile = + Option(System.getProperty(KeyStoreSystemProperty, "")) + .filter(_.nonEmpty) + .map(new File(_)) + + val keyStore = KeyStore.getInstance(KeyStore.getDefaultType) + keyStore.load( + propertyKeyStoreFile.map(new FileInputStream(_)).orNull, + System.getProperty(KeyStorePasswordSystemProperty, "").toCharArray + ) + keyStore + } +}