Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ developers in ThisBuild := List(Developer(id="doriordan", name="David ORiordan",

lazy val commonSettings = Seq(
organization := "io.skuber",
crossScalaVersions := Seq("2.12.17", "2.13.10"),
scalaVersion := "2.13.10",
crossScalaVersions := Seq("2.12.20", "2.13.17"),
scalaVersion := "2.13.17",
publishTo := sonatypePublishToBundle.value,
pomIncludeRepository := { _ => false },
Test / classLoaderLayeringStrategy := ClassLoaderLayeringStrategy.Flat
Expand Down
50 changes: 25 additions & 25 deletions client/src/main/scala/skuber/api/client/exec/PodExecImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package skuber.api.client.exec

import akka.actor.ActorSystem
import akka.http.scaladsl.model.headers.RawHeader
import akka.http.scaladsl.model.{HttpHeader, StatusCodes, Uri, ws}
import akka.http.scaladsl.model.{StatusCodes, Uri, ws}
import akka.http.scaladsl.{ConnectionContext, Http}
import akka.stream.scaladsl.{Flow, GraphDSL, Keep, Partition, Sink, Source}
import akka.stream.SinkShape
Expand Down Expand Up @@ -62,10 +62,6 @@ object PodExecImpl {
.withPath(Uri.Path(s"/api/v1/namespaces/${requestContext.namespaceName}/pods/$podName/exec"))
.withQuery(Uri.Query(queries: _*))

// Compose headers
var headers: List[HttpHeader] = List(RawHeader("Accept", "*/*"))
headers ++= HTTPRequestAuth.getAuthHeader(requestContext.requestAuth).map(a => List(a)).getOrElse(List())

// Convert `String` to `ByteString`, then prepend channel bytes
val source: Source[ws.Message, Promise[Option[ws.Message]]] = maybeStdin.getOrElse(Source.empty).viaMat(Flow[String].map { s =>
ws.BinaryMessage(ByteString(0).concat(ByteString(s)))
Expand Down Expand Up @@ -97,31 +93,35 @@ object PodExecImpl {
// Make a flow from the source to the sink
val flow: Flow[ws.Message, ws.Message, Promise[Option[ws.Message]]] = Flow.fromSinkAndSourceMat(sink, source)(Keep.right)

// upgradeResponse completes or fails when the connection succeeds or fails
// and promise controls the connection close timing
val (upgradeResponse, promise) = Http().singleWebSocketRequest(ws.WebSocketRequest(uri, headers, subprotocol = Option("channel.k8s.io")), flow, connectionContext)

val connected = upgradeResponse.map { upgrade =>
for {
authHeaders <- HTTPRequestAuth.getAuthHeaderAsync(requestContext.requestAuth)
headers = List(RawHeader("Accept", "*/*")) ++ authHeaders.toList
// upgradeResponse completes or fails when the connection succeeds or fails
// and promise controls the connection close timing
(upgradeResponseFuture, promise) = Http().singleWebSocketRequest(ws.WebSocketRequest(uri, headers, subprotocol = Option("channel.k8s.io")), flow, connectionContext)
upgrade <- upgradeResponseFuture
// just like a regular http request we can access response status which is available via upgrade.response.status
// status code 101 (Switching Protocols) indicates that server support WebSockets
if (upgrade.response.status == StatusCodes.SwitchingProtocols) {
Done
_ <- if (upgrade.response.status == StatusCodes.SwitchingProtocols) {
Future.successful(Done)
} else {
val message = upgrade.response.entity.toStrict(1000.millis).map(_.data.utf8String)
throw new K8SException(Status(message =
for {
entity <- upgrade.response.entity.toStrict(1000.millis)
_ <- Future.failed(new K8SException(Status(message =
Some(s"Connection failed with status ${upgrade.response.status}"),
details = Some(message), code = Some(upgrade.response.status.intValue())))
details = Some(entity.data.utf8String), code = Some(upgrade.response.status.intValue()))))
} yield ()
}
}

val close = maybeClose.getOrElse(Promise.successful(()))
connected.foreach { _ =>
requestContext.log.info(s"Connected to container $containerPrintName of pod $podName")
close.future.foreach { _ =>
requestContext.log.info(s"Close the connection of container $containerPrintName of pod $podName")
promise.success(None)
close = maybeClose.getOrElse(Promise.successful(()))
_ = {
requestContext.log.info(s"Connected to container ${containerPrintName} of pod ${podName}")
close.future.foreach { _ =>
requestContext.log.info(s"Close the connection of container ${containerPrintName} of pod ${podName}")
promise.trySuccess(None)
}
}
}
Future.sequence(Seq(connected, close.future, promise.future)).map { _ => () }
_ <- close.future
_ <- promise.future
} yield ()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class KubernetesClientImpl private[client] (
rd: ResourceDefinition[_],
nameComponent: Option[String],
query: Option[Uri.Query] = None,
namespace: String = namespaceName): HttpRequest =
namespace: String = namespaceName): Future[HttpRequest] =
{
val nsPathComponent = if (rd.spec.scope == ResourceSpecification.Scope.Namespaced) {
Some("namespaces/" + namespace)
Expand Down Expand Up @@ -112,7 +112,7 @@ class KubernetesClientImpl private[client] (
}

val req = requestMaker(uri, method)
HTTPRequestAuth.addAuth(req, requestAuth)
HTTPRequestAuth.addAuthAsync(req, requestAuth)
}

private[skuber] def logInfo(enabledLogEvent: Boolean, msg: => String)(implicit lc: LoggingContext) =
Expand Down Expand Up @@ -225,9 +225,9 @@ class KubernetesClientImpl private[client] (
val marshal = Marshal(obj)
for {
requestEntity <- marshal.to[RequestEntity]
httpRequest = buildRequest(method, rd, nameComponent, namespace = targetNamespace)
.withEntity(requestEntity.withContentType(MediaTypes.`application/json`))
newOrUpdatedResource <- makeRequestReturningObjectResource[O](httpRequest)
httpRequest <- buildRequest(method, rd, nameComponent, namespace = targetNamespace)
requestWithEntity = httpRequest.withEntity(requestEntity.withContentType(MediaTypes.`application/json`))
newOrUpdatedResource <- makeRequestReturningObjectResource[O](requestWithEntity)
} yield newOrUpdatedResource
}

Expand Down Expand Up @@ -309,8 +309,8 @@ class KubernetesClientImpl private[client] (
private def listInNamespace[L <: ListResource[_]](theNamespace: String, rd: ResourceDefinition[_])(
implicit fmt: Format[L], lc: LoggingContext): Future[L] =
{
val req = buildRequest(HttpMethods.GET, rd, None, namespace = theNamespace)
makeRequestReturningListResource[L](req)
buildRequest(HttpMethods.GET, rd, None, namespace = theNamespace)
.flatMap(makeRequestReturningListResource[L])
}

/*
Expand Down Expand Up @@ -347,8 +347,8 @@ class KubernetesClientImpl private[client] (
val optsInfo = maybeOptions map { opts => s" with options '${opts.asMap.toString}'" } getOrElse ""
logDebug(s"[List request: resources of kind '${rd.spec.names.kind}'${optsInfo}")
}
val req = buildRequest(HttpMethods.GET, rd, None, query = queryOpt)
makeRequestReturningListResource[L](req)
buildRequest(HttpMethods.GET, rd, None, query = queryOpt)
.flatMap(makeRequestReturningListResource[L])
}

override def getOption[O <: ObjectResource](name: String)(
Expand Down Expand Up @@ -376,8 +376,8 @@ class KubernetesClientImpl private[client] (
private[api] def _get[O <: ObjectResource](name: String, namespace: String = namespaceName)(
implicit fmt: Format[O], rd: ResourceDefinition[O], lc: LoggingContext): Future[O] =
{
val req = buildRequest(HttpMethods.GET, rd, Some(name), namespace = namespace)
makeRequestReturningObjectResource[O](req)
buildRequest(HttpMethods.GET, rd, Some(name), namespace = namespace)
.flatMap(makeRequestReturningObjectResource[O])
}

override def delete[O <: ObjectResource](name: String, gracePeriodSeconds: Int = -1)(
Expand All @@ -394,11 +394,11 @@ class KubernetesClientImpl private[client] (
val marshalledOptions = Marshal(options)
for {
requestEntity <- marshalledOptions.to[RequestEntity]
request = buildRequest(HttpMethods.DELETE, rd, Some(name))
.withEntity(requestEntity.withContentType(MediaTypes.`application/json`))
response <- invoke(request)
_ <- checkResponseStatus(response)
_ <- ignoreResponseBody(response)
request <- buildRequest(HttpMethods.DELETE, rd, Some(name))
requestWithEntity = request.withEntity(requestEntity.withContentType(MediaTypes.`application/json`))
response <- invoke(requestWithEntity)
_ <- checkResponseStatus(response)
_ <- ignoreResponseBody(response)
} yield ()
}

Expand All @@ -424,8 +424,8 @@ class KubernetesClientImpl private[client] (
val lsInfo = maybeLabelSelector map { ls => s" with label selector '${ls.toString}'" } getOrElse ""
logDebug(s"[Delete request: resources of kind '${rd.spec.names.kind}'${lsInfo}")
}
val req = buildRequest(HttpMethods.DELETE, rd, None, query = queryOpt)
makeRequestReturningListResource[L](req)
buildRequest(HttpMethods.DELETE, rd, None, query = queryOpt)
.flatMap(makeRequestReturningListResource[L])
}

override def getPodLogSource(name: String, queryParams: Pod.LogQueryParams, namespace: Option[String] = None)(
Expand All @@ -440,16 +440,17 @@ class KubernetesClientImpl private[client] (
}
val nameComponent=s"${name}/log"
val rd = implicitly[ResourceDefinition[Pod]]
val request = buildRequest(HttpMethods.GET, rd, Some(nameComponent), query, targetNamespace)
invokeLog(request).flatMap { response =>
val statusOptFut = checkResponseStatus(response)
statusOptFut map {
for {
request <- buildRequest(HttpMethods.GET, rd, Some(nameComponent), query, targetNamespace)
response <- invokeLog(request)
statusOpt <- checkResponseStatus(response)
_ <- statusOpt match {
case Some(status) =>
throw new K8SException(status)
case _ =>
response.entity.dataBytes
Future.failed(new K8SException(status))
case None =>
Future.successful(())
}
}
} yield response.entity.dataBytes
}


Expand Down Expand Up @@ -523,8 +524,8 @@ class KubernetesClientImpl private[client] (
override def getScale[O <: ObjectResource](objName: String)(
implicit rd: ResourceDefinition[O], sc: Scale.SubresourceSpec[O], lc: LoggingContext) : Future[Scale] =
{
val req = buildRequest(HttpMethods.GET, rd, Some(objName+ "/scale"))
makeRequestReturningObjectResource[Scale](req)
buildRequest(HttpMethods.GET, rd, Some(objName + "/scale"))
.flatMap(makeRequestReturningObjectResource[Scale])
}

@deprecated("use getScale followed by updateScale instead")
Expand All @@ -546,9 +547,9 @@ class KubernetesClientImpl private[client] (
val marshal = Marshal(scale)
for {
requestEntity <- marshal.to[RequestEntity]
httpRequest = buildRequest(HttpMethods.PUT, rd, Some(s"${objName}/scale"))
.withEntity(requestEntity.withContentType(MediaTypes.`application/json`))
scaledResource <- makeRequestReturningObjectResource[Scale](httpRequest)
httpRequest <- buildRequest(HttpMethods.PUT, rd, Some(s"${objName}/scale"))
requestWithEntity = httpRequest.withEntity(requestEntity.withContentType(MediaTypes.`application/json`))
scaledResource <- makeRequestReturningObjectResource[Scale](requestWithEntity)
} yield scaledResource
}

Expand All @@ -568,9 +569,9 @@ class KubernetesClientImpl private[client] (
val marshal = Marshal(patchData)
for {
requestEntity <- marshal.to[RequestEntity]
httpRequest = buildRequest(HttpMethods.PATCH, rd, Some(name), namespace = targetNamespace)
.withEntity(requestEntity.withContentType(contentType))
newOrUpdatedResource <- makeRequestReturningObjectResource[O](httpRequest)
httpRequest <- buildRequest(HttpMethods.PATCH, rd, Some(name), namespace = targetNamespace)
requestWithEntity = httpRequest.withEntity(requestEntity.withContentType(contentType))
newOrUpdatedResource <- makeRequestReturningObjectResource[O](requestWithEntity)
} yield newOrUpdatedResource
}

Expand All @@ -590,16 +591,19 @@ class KubernetesClientImpl private[client] (
implicit rd: ResourceDefinition[O], fmt: Format[O], lc:LoggingContext): Future[O] =
{
val patchRequestEntity = HttpEntity.Strict(`application/merge-patch+json`, ByteString(patch))
val httpRequest = buildRequest(HttpMethods.PATCH, rd, Some(obj.name)).withEntity(patchRequestEntity)
makeRequestReturningObjectResource[O](httpRequest)
for {
httpRequest <- buildRequest(HttpMethods.PATCH, rd, Some(obj.name))
requestWithEntity = httpRequest.withEntity(patchRequestEntity)
result <- makeRequestReturningObjectResource[O](requestWithEntity)
} yield result
}

// get API versions supported by the cluster
override def getServerAPIVersions(implicit lc: LoggingContext): Future[List[String]] = {
val url = clusterServer + "/api"
val noAuthReq = requestMaker(Uri(url), HttpMethods.GET)
val request = HTTPRequestAuth.addAuth(noAuthReq, requestAuth)
for {
request <- HTTPRequestAuth.addAuthAsync(noAuthReq, requestAuth)
response <- invoke(request)
apiVersionResource <- toKubernetesResponse[APIVersions](response)
} yield apiVersionResource.versions
Expand Down
9 changes: 7 additions & 2 deletions client/src/main/scala/skuber/api/client/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@ package skuber.api

import java.time.Instant
import java.util.UUID

import akka.NotUsed
import akka.actor.ActorSystem
import akka.http.scaladsl.model._
import akka.stream.scaladsl.Flow
import com.typesafe.config.{Config, ConfigFactory}
import com.typesafe.config.{ Config, ConfigFactory }
import play.api.libs.functional.syntax._
import play.api.libs.json.Reads._
import play.api.libs.json._
Expand All @@ -17,6 +16,8 @@ import scala.util.Try
import skuber.ObjectResource
import skuber.api.client.impl.KubernetesClientImpl

import scala.concurrent.Future

/**
* @author David O'Riordan
*/
Expand All @@ -42,6 +43,10 @@ package object client {
def accessToken: String
}

trait AsyncAccessTokenAuth extends AuthInfo {
def accessToken(): Future[String]
}

object NoAuth extends AuthInfo {
override def toString: String = "NoAuth"
}
Expand Down
26 changes: 22 additions & 4 deletions client/src/main/scala/skuber/api/security/HTTPRequestAuth.scala
Original file line number Diff line number Diff line change
@@ -1,23 +1,41 @@
package skuber.api.security

import akka.http.scaladsl.model.{HttpHeader, HttpRequest}
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials, OAuth2BearerToken}
import akka.http.scaladsl.model.{ HttpHeader, HttpRequest }
import akka.http.scaladsl.model.headers.{ Authorization, BasicHttpCredentials, OAuth2BearerToken }
import skuber.api.client._

import scala.concurrent.{ ExecutionContext, Future }

/**
* @author David O'Riordan
*/
object HTTPRequestAuth {


@deprecated("2.6.8", "Use addAuthAsync instead")
def addAuth(request: HttpRequest, auth: AuthInfo) : HttpRequest = {
getAuthHeader(auth).map(request.addHeader).getOrElse(request)
}

def addAuthAsync(request: HttpRequest, auth: AuthInfo): Future[HttpRequest] = {
getAuthHeaderAsync(auth).map(_.map(request.addHeader).getOrElse(request))(ExecutionContext.parasitic)
}

@deprecated("2.6.8", "Use addAuthAsync instead")
def getAuthHeader(auth: AuthInfo) : Option[HttpHeader] = {
auth match {
case NoAuth | _: CertAuth => None
case BasicAuth(user, password) => Some(Authorization(BasicHttpCredentials(user,password)))
case auth: AccessTokenAuth => Some(Authorization(OAuth2BearerToken(auth.accessToken)))
case _: AsyncAccessTokenAuth => sys.error("Async auth does not work with getAuthHeader")
}
}
}

def getAuthHeaderAsync(auth: AuthInfo): Future[Option[HttpHeader]] = {
auth match {
case NoAuth | _: CertAuth => Future.successful(None)
case BasicAuth(user, password) => Future.successful(Some(Authorization(BasicHttpCredentials(user, password))))
case auth: AccessTokenAuth => Future.successful(Some(Authorization(OAuth2BearerToken(auth.accessToken))))
case asyncAuth: AsyncAccessTokenAuth => asyncAuth.accessToken().map(token => Some(Authorization(OAuth2BearerToken(token))))(ExecutionContext.parasitic)
}
}
}
33 changes: 11 additions & 22 deletions client/src/main/scala/skuber/api/watch/Watch.scala
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,13 @@ object Watch {
watch = Some(true),
fieldSelector = nameFieldSelector
)
val request = context.buildRequest(HttpMethods.GET, rd, None, query = Some(Uri.Query(watchOptions.asMap)))
val responseFut = context.invokeWatch(request)
toFutureWatchEventSource(context, responseFut, bufSize, errorHandler)

implicit val ec: ExecutionContext = context.actorSystem.dispatcher

for {
request <- context.buildRequest(HttpMethods.GET, rd, None, query = Some(Uri.Query(watchOptions.asMap)))
response <- context.invokeWatch(request)
} yield BytesToWatchEventSource(context, response.entity.dataBytes, bufSize, errorHandler)
}

/**
Expand All @@ -60,28 +64,13 @@ object Watch {
context.logInfo(context.logConfig.logRequestBasic, s"creating skuber watch on kind ${rd.spec.names.kind}")

val watchOptions=ListOptions(resourceVersion = sinceResourceVersion, watch = Some(true))
val request = context.buildRequest(HttpMethods.GET, rd, None, query = Some(Uri.Query(watchOptions.asMap)))
val responseFut = context.invokeWatch(request)
toFutureWatchEventSource(context, responseFut, bufSize, errorHandler)
}

/**
* Create a (future) Source of watch events from a (future) response to a watch request
* @param context the applicable request context
* @param eventStreamResponseFut the response which will contain the event stream for the source
* @param bufSize maximum size of each event in the stream (in bytes)
* @param format Play json formatter for the applicable Kubernetes type, used to read each event object
* @tparam O the Kubernetes kind of each events object
* @return a Future which will eventually return a Source of events
*/
private def toFutureWatchEventSource[O <: ObjectResource](context: KubernetesClientImpl, eventStreamResponseFut: Future[HttpResponse], bufSize: Int, errorHandler: Option[String => _])(
implicit format: Format[O],lc: LoggingContext): Future[Source[WatchEvent[O], _]] =
{
implicit val ec: ExecutionContext = context.actorSystem.dispatcher

eventStreamResponseFut.map { eventStreamResponse =>
BytesToWatchEventSource(context, eventStreamResponse.entity.dataBytes, bufSize, errorHandler)
}
for {
request <- context.buildRequest(HttpMethods.GET, rd, None, query = Some(Uri.Query(watchOptions.asMap)))
response <- context.invokeWatch(request)
} yield BytesToWatchEventSource(context, response.entity.dataBytes, bufSize, errorHandler)
}
}

Loading