Skip to content

Commit

Permalink
Merge pull request #347 from hagay3/feature/hagai/addSkuberExecConfig
Browse files Browse the repository at this point in the history
Feature/hagai/add skuber exec config
  • Loading branch information
hagay3 authored Nov 18, 2023
2 parents 11ba932 + a916100 commit 02cb3a4
Show file tree
Hide file tree
Showing 10 changed files with 410 additions and 237 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest]
scala: [2.12.13, 2.13.12, 3.2.0]
scala: [2.12.13, 2.13.12, 3.3.1]
java: [temurin@8, temurin@17]
runs-on: ${{ matrix.os }}
steps:
Expand Down Expand Up @@ -115,12 +115,12 @@ jobs:
tar xf targets.tar
rm targets.tar
- name: Download target directories (3.2.0)
- name: Download target directories (3.3.1)
uses: actions/download-artifact@v3
with:
name: target-${{ matrix.os }}-3.2.0-${{ matrix.java }}
name: target-${{ matrix.os }}-3.3.1-${{ matrix.java }}

- name: Inflate target directories (3.2.0)
- name: Inflate target directories (3.3.1)
run: |
tar xf targets.tar
rm targets.tar
Expand Down
4 changes: 2 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ resolvers += "Typesafe Releases" at "https://repo.typesafe.com/typesafe/releases

val scala12Version = "2.12.13"
val scala13Version = "2.13.12"
val scala3Version = "3.2.0"
val scala3Version = "3.3.1"

val currentScalaVersion = scala13Version
val currentScalaVersion = scala3Version

ThisBuild / scalaVersion := currentScalaVersion

Expand Down
37 changes: 33 additions & 4 deletions client/src/main/scala/skuber/api/Configuration.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ package skuber.api
import org.yaml.snakeyaml.Yaml
import skuber.Namespace
import skuber.api.client._
import skuber.api.client.token.{FileTokenAuthRefreshable, FileTokenConfiguration}
import skuber.api.client.token.{ExecAuthConfig, ExecAuthRefreshable, FileTokenAuthRefreshable, FileTokenConfiguration}
import skuber.config.SkuberConfig

import java.net.URL
import java.time.Instant
import java.time.format.DateTimeFormatter
import java.util
import java.util.{Base64, Date}
import scala.collection.JavaConverters._
import scala.concurrent.duration.{Duration, DurationInt}
Expand Down Expand Up @@ -178,6 +178,32 @@ object Configuration {
}
}

def parseExecConfig(userConfig: YamlMap): Option[ExecAuthRefreshable] = {
optionalValueAt[YamlMap](userConfig, "exec").flatMap { yamlExec =>
val args = optionalValueAt[util.ArrayList[String]](yamlExec, "args").map(_.asScala.toList).getOrElse(List.empty)
val env = optionalValueAt[util.ArrayList[util.Map[String, String]]](yamlExec, "env").map(_.asScala.toList.map(_.asScala.toMap)).getOrElse(List.empty)
val envVariables = env.flatMap { envSingle =>
val nameOpt = envSingle.get("name")
val valueOpt = envSingle.get("value")
(nameOpt, valueOpt) match {
case (Some(name), Some(value)) => Some(name -> value)
case _ => None
}
}.toMap

val commandOpt: Option[String] = optionalValueAt[String](yamlExec, "command")

commandOpt.map { command =>
val config = ExecAuthConfig(
cmd = command,
args = args,
envVariables = envVariables
)
ExecAuthRefreshable(config)
}
}
}

val maybeAuth = optionalValueAt[YamlMap](userConfig, "auth-provider") match {
case Some(authProvider) => authProviderRead(authProvider)
case None =>
Expand All @@ -197,7 +223,11 @@ object Configuration {
}
}

maybeAuth.getOrElse(NoAuth)
val maybeExecAuth = maybeAuth orElse {
parseExecConfig(userConfig)
}

maybeExecAuth.getOrElse(NoAuth)
}
val k8sAuthInfoMap = topLevelYamlToK8SConfigMap("user", toK8SAuthInfo)

Expand Down Expand Up @@ -226,7 +256,6 @@ object Configuration {
* <p>Follows official golang client logic
*
* @return Try[Configuration]
*
* @see https://kubernetes.io/docs/tasks/run-application/access-api-from-pod/
* https://github.com/kubernetes/client-go/blob/master/rest/config.go#L313
* https://github.com/kubernetes-client/java/blob/master/util/src/main/java/io/kubernetes/client/util/ClientBuilder.java#L134
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package skuber.api.client.token

import java.nio.charset.StandardCharsets
import java.time.{ZoneId, ZonedDateTime}
import java.util.TimeZone
import org.apache.commons.io.IOUtils
import org.joda.time.{DateTime, DateTimeZone}
import play.api.libs.json.Json
import skuber.api.client.AuthProviderRefreshableAuth
import scala.collection.JavaConverters._

// https://kubernetes.io/docs/reference/config-api/kubeconfig.v1/#ExecConfig
final case class ExecAuthRefreshable(config: ExecAuthConfig) extends AuthProviderRefreshableAuth {
override val name = "exec"

@volatile private var cachedToken: Option[RefreshableToken] = None

override def refreshToken: RefreshableToken = {
val output = generateToken
val parsed = Json.parse(output).as[ExecCredential]
val refreshableToken = toRefreshableToken(parsed)
cachedToken = Some(refreshableToken)
refreshableToken
}

def accessToken: String = this.synchronized {
cachedToken match {
case Some(token) if isTokenExpired(token) =>
refreshToken.accessToken
case None =>
refreshToken.accessToken
case Some(token) =>
token.accessToken
}
}

override def toString =
"""ExecAuthRefreshable(accessToken=<redacted>)""".stripMargin

override def isTokenExpired(refreshableToken: RefreshableToken): Boolean = {
DateTime.now(DateTimeZone.UTC).isAfter(refreshableToken.expiry)
}

override def generateToken: String = config.execute()

private def toRefreshableToken(execCredential: ExecCredential): RefreshableToken = {
val utc = ZoneId.of("UTC")
val now = ZonedDateTime.now(utc)
val expiration = execCredential.status.expirationTimestamp.getOrElse(now.plusYears(1))
val expirationDateTime = new DateTime(expiration.toInstant.toEpochMilli, DateTimeZone.forTimeZone(TimeZone.getTimeZone(utc)))

RefreshableToken(execCredential.status.token, expirationDateTime)
}
}

final case class ExecAuthConfig(cmd: String,
args: List[String],
envVariables: Map[String, String]) {
def execute(): String = {
val process = new java.lang.ProcessBuilder((Seq(cmd) ++ args).toList.asJava)
envVariables.map { case (name, value) => process.environment().put(name, value)}
val output = IOUtils.toString(process.start.getInputStream, StandardCharsets.UTF_8)
output
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package skuber.api.client.token

import play.api.libs.json.{Json, OFormat}

final case class ExecCredential(status: ExecCredentialStatus)

object ExecCredential {
implicit val execCredentialFmt: OFormat[ExecCredential] = Json.format[ExecCredential]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package skuber.api.client.token

import play.api.libs.json.{Json, OFormat}
import skuber.Timestamp

final case class ExecCredentialStatus(expirationTimestamp: Option[Timestamp], token: String)

object ExecCredentialStatus {
implicit val execCredentialStatusFmt: OFormat[ExecCredentialStatus] = Json.format[ExecCredentialStatus]
}
Loading

0 comments on commit 02cb3a4

Please sign in to comment.