From 032da55a7934f86281299b19b6b21072330d69c1 Mon Sep 17 00:00:00 2001 From: hagay3 Date: Tue, 7 Jun 2022 08:24:41 +0300 Subject: [PATCH 01/12] AwsAuthRefreshable - aws refreshable token --- build.sbt | 5 ++ .../scala/skuber/api/client/package.scala | 9 +- .../api/client/token/AwsAuthRefreshable.scala | 86 +++++++++++++++++++ 3 files changed, 94 insertions(+), 6 deletions(-) create mode 100644 client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala diff --git a/build.sbt b/build.sbt index 4641afbd..d74d6db7 100644 --- a/build.sbt +++ b/build.sbt @@ -37,6 +37,10 @@ val logback = "ch.qos.logback" % "logback-classic" % "1.2.11" % Runtime // the Json formatters are based on Play Json val playJson = "com.typesafe.play" %% "play-json" % "2.9.2" +val awsJavaSdkCore = "com.amazonaws" % "aws-java-sdk-core" % "1.12.233" +val awsJavaSdkSts = "com.amazonaws" % "aws-java-sdk-sts" % "1.12.233" +val apacheCommonsLogging = "commons-logging" % "commons-logging" % "1.2" + // Need Java 8 or later as the java.time package is used to represent K8S timestamps scalacOptions += "-target:jvm-1.8" @@ -138,6 +142,7 @@ lazy val skuberSettings = Seq( name := "skuber", libraryDependencies ++= Seq( akkaHttp, akkaStream, playJson, snakeYaml, commonsIO, commonsCodec, bouncyCastle, + awsJavaSdkCore, awsJavaSdkSts, apacheCommonsLogging, scalaCheck % Test, specs2 % Test, mockito % Test, akkaStreamTestKit % Test, scalaTest % Test ).map(_.exclude("commons-logging", "commons-logging")) diff --git a/client/src/main/scala/skuber/api/client/package.scala b/client/src/main/scala/skuber/api/client/package.scala index 6cc5b5ac..d31c7adb 100644 --- a/client/src/main/scala/skuber/api/client/package.scala +++ b/client/src/main/scala/skuber/api/client/package.scala @@ -2,21 +2,18 @@ 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.Materializer import akka.stream.scaladsl.Flow import com.typesafe.config.{Config, ConfigFactory} import play.api.libs.functional.syntax._ import play.api.libs.json.Reads._ import play.api.libs.json._ - +import skuber.ObjectResource +import skuber.api.client.impl.KubernetesClientImpl import scala.sys.SystemProperties import scala.util.Try -import skuber.{LabelSelector, ObjectResource} -import skuber.api.client.impl.KubernetesClientImpl /** * @author David O'Riordan @@ -80,7 +77,7 @@ package object client { .mkString } - sealed trait AuthProviderAuth extends AccessTokenAuth { + trait AuthProviderAuth extends AccessTokenAuth { def name: String } diff --git a/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala b/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala new file mode 100644 index 00000000..209348ff --- /dev/null +++ b/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala @@ -0,0 +1,86 @@ +package skuber.api.client.token + +import java.net.URI +import java.util.{Base64, Date} +import com.amazonaws.DefaultRequest +import com.amazonaws.auth.presign.{PresignerFacade, PresignerParams} +import com.amazonaws.auth._ +import com.amazonaws.http.HttpMethodName +import com.amazonaws.internal.auth.DefaultSignerProvider +import com.amazonaws.regions.Regions +import com.amazonaws.services.securitytoken.model.GetCallerIdentityRequest +import com.amazonaws.services.securitytoken.{AWSSecurityTokenServiceClient, AWSSecurityTokenServiceClientBuilder} +import org.joda.time.DateTime +import skuber.K8SException +import skuber.api.client._ +import scala.concurrent.duration._ + +final case class AwsAuthRefreshable(clusterName: String, + region: Regions, + cachedAccessToken: Option[RefreshableToken] = None, + refreshInterval: Duration = 60.minutes) extends AuthProviderAuth { + + @volatile private var refresh: Option[RefreshableToken] = cachedAccessToken.map(token => RefreshableToken(token.accessToken, token.expiry)) + + private def refreshGcpToken(): RefreshableToken = { + val token = generateAwsToken + val refreshedToken = RefreshableToken(token, DateTime.now.plus(refreshInterval.toSeconds)) + refresh = Some(refreshedToken) + refreshedToken + } + + private def generateAwsToken: String = { + try { + val credentialsProvider = new AWSStaticCredentialsProvider(new DefaultAWSCredentialsProviderChain().getCredentials) + + val tokenService: AWSSecurityTokenServiceClient = AWSSecurityTokenServiceClientBuilder + .standard() + .withRegion(region) + .withCredentials(credentialsProvider) + .build().asInstanceOf[AWSSecurityTokenServiceClient] + + val callerIdentityRequestDefaultRequest = new DefaultRequest[GetCallerIdentityRequest](new GetCallerIdentityRequest(), "sts") + val uri = new URI("https", "sts.amazonaws.com", null, null) + callerIdentityRequestDefaultRequest.setResourcePath("/") + callerIdentityRequestDefaultRequest.setEndpoint(uri) + callerIdentityRequestDefaultRequest.setHttpMethod(HttpMethodName.GET) + callerIdentityRequestDefaultRequest.addParameter("Action", "GetCallerIdentity") + callerIdentityRequestDefaultRequest.addParameter("Version", "2011-06-15") + callerIdentityRequestDefaultRequest.addHeader("x-k8s-aws-id", clusterName) + val signer = SignerFactory.createSigner(SignerFactory.VERSION_FOUR_SIGNER, new SignerParams("sts", region.getName)) + val signerProvider = new DefaultSignerProvider(tokenService, signer) + val presignerParams = new PresignerParams( + uri, + credentialsProvider, + signerProvider, + SdkClock.STANDARD) + val presignerFacade = new PresignerFacade(presignerParams) + val url = presignerFacade.presign(callerIdentityRequestDefaultRequest, new Date()) + val encodedUrl = Base64.getUrlEncoder.withoutPadding().encodeToString(url.toString.getBytes) + + s"k8s-aws-v1.$encodedUrl" + } catch { + case e: Exception => + throw new K8SException(Status(reason = Option(e.getMessage))) + } + } + + override def name: String = "aws" + + def accessToken: String = this.synchronized { + refresh match { + case Some(expired) if expired.expired => + refreshGcpToken().accessToken + case None => + refreshGcpToken().accessToken + case Some(token) => + token.accessToken + } + } + + override def toString: String = """AwsAuth(accessToken=)""".stripMargin +} + +final case class RefreshableToken(accessToken: String, expiry: DateTime) { + def expired: Boolean = expiry.isBefore(System.currentTimeMillis) +} From ac4dff88ef4d8a2f23e4ffe20621d05e762ccd50 Mon Sep 17 00:00:00 2001 From: hagay3 Date: Fri, 10 Jun 2022 19:04:30 +0300 Subject: [PATCH 02/12] aws token refreshable - phase 2 --- .../main/scala/skuber/api/Configuration.scala | 29 ++++++++---- .../scala/skuber/api/client/Cluster.scala | 3 +- .../api/client/token/AwsAuthRefreshable.scala | 47 ++++++++++++++----- .../scala/skuber/api/ConfigurationSpec.scala | 6 +-- .../skuber/examples/auth/AwsAuthExample.scala | 38 +++++++++++++++ 5 files changed, 99 insertions(+), 24 deletions(-) create mode 100644 examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala diff --git a/client/src/main/scala/skuber/api/Configuration.scala b/client/src/main/scala/skuber/api/Configuration.scala index e5bbbd06..1da703f0 100644 --- a/client/src/main/scala/skuber/api/Configuration.scala +++ b/client/src/main/scala/skuber/api/Configuration.scala @@ -4,16 +4,15 @@ import java.io.File import java.net.URL import java.time.Instant import java.time.format.DateTimeFormatter - import scala.collection.JavaConverters._ import scala.util.Try import scala.util.Failure import java.util.{Base64, Date} - +import com.amazonaws.regions.Regions import org.yaml.snakeyaml.Yaml import skuber.Namespace import skuber.api.client._ - +import skuber.api.client.token.AwsAuthRefreshable import scala.io.Source /** @@ -149,22 +148,27 @@ object Configuration { } } - def topLevelYamlToK8SConfigMap[K8SConfigKind](kind: String, toK8SConfig: YamlMap=> K8SConfigKind) = - topLevelList(kind + "s").asScala.map(item => name(item) -> toK8SConfig(child(item, kind))).toMap + def topLevelYamlToK8SConfigMap[K8SConfigKind](kind: String, toK8SConfig: (YamlMap, String) => K8SConfigKind): Map[String, K8SConfigKind] = { + topLevelList(kind + "s").asScala.map{ item => + val clusterName = name(item) + name(item) -> toK8SConfig(child(item, kind), clusterName) + }.toMap + } - def toK8SCluster(clusterConfig: YamlMap) = + def toK8SCluster(clusterConfig: YamlMap, clusterName: String) = Cluster( apiVersion=valueAt(clusterConfig, "api-version", Some("v1")), server=valueAt(clusterConfig,"server",Some("http://localhost:8001")), insecureSkipTLSVerify=valueAt(clusterConfig,"insecure-skip-tls-verify",Some(false)), - certificateAuthority=pathOrDataValueAt(clusterConfig, "certificate-authority","certificate-authority-data") + certificateAuthority=pathOrDataValueAt(clusterConfig, "certificate-authority","certificate-authority-data"), + clusterName = Some(clusterName) ) val k8sClusterMap = topLevelYamlToK8SConfigMap("cluster", toK8SCluster) - def toK8SAuthInfo(userConfig:YamlMap): AuthInfo = { + def toK8SAuthInfo(userConfig:YamlMap, clusterName: String): AuthInfo = { def authProviderRead(authProvider: YamlMap): Option[AuthProviderAuth] = { val config = child(authProvider, "config") @@ -180,6 +184,13 @@ object Configuration { cmdArgs = valueAt(config, "cmd-args") ) ) + case "aws" => + Some( + AwsAuthRefreshable( + clusterName = optionalValueAt(config, "cluster-name"), + region = optionalValueAt[String](config, "region").flatMap(rg => Try(Regions.fromName(rg)).toOption) + ) + ) case _ => None } } @@ -207,7 +218,7 @@ object Configuration { } val k8sAuthInfoMap = topLevelYamlToK8SConfigMap("user", toK8SAuthInfo) - def toK8SContext(contextConfig: YamlMap) = { + def toK8SContext(contextConfig: YamlMap, clusterName: String) = { val cluster=contextConfig.asScala.get("cluster").filterNot(_ == "").map { clusterName => k8sClusterMap.get(clusterName.asInstanceOf[String]).get }.getOrElse(Cluster()) diff --git a/client/src/main/scala/skuber/api/client/Cluster.scala b/client/src/main/scala/skuber/api/client/Cluster.scala index a56ad187..bcead127 100644 --- a/client/src/main/scala/skuber/api/client/Cluster.scala +++ b/client/src/main/scala/skuber/api/client/Cluster.scala @@ -9,5 +9,6 @@ case class Cluster( apiVersion: String = "v1", server: String = defaultApiServerURL, insecureSkipTLSVerify: Boolean = false, - certificateAuthority: Option[PathOrData] = None + certificateAuthority: Option[PathOrData] = None, + clusterName: Option[String] = None ) diff --git a/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala b/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala index 209348ff..487387ad 100644 --- a/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala +++ b/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala @@ -3,8 +3,8 @@ package skuber.api.client.token import java.net.URI import java.util.{Base64, Date} import com.amazonaws.DefaultRequest -import com.amazonaws.auth.presign.{PresignerFacade, PresignerParams} import com.amazonaws.auth._ +import com.amazonaws.auth.presign.{PresignerFacade, PresignerParams} import com.amazonaws.http.HttpMethodName import com.amazonaws.internal.auth.DefaultSignerProvider import com.amazonaws.regions.Regions @@ -14,18 +14,42 @@ import org.joda.time.DateTime import skuber.K8SException import skuber.api.client._ import scala.concurrent.duration._ +import scala.util.Try -final case class AwsAuthRefreshable(clusterName: String, - region: Regions, - cachedAccessToken: Option[RefreshableToken] = None, +final case class AwsAuthRefreshable(clusterName: Option[String] = None, + region: Option[Regions] = None, refreshInterval: Duration = 60.minutes) extends AuthProviderAuth { - @volatile private var refresh: Option[RefreshableToken] = cachedAccessToken.map(token => RefreshableToken(token.accessToken, token.expiry)) + @volatile private var cachedToken: Option[RefreshableToken] = None + + private val clusterNameToRefresh: String = { + clusterName.getOrElse { + defaultK8sConfig.currentContext.cluster.clusterName.getOrElse { + throw new K8SException(Status(reason = + Some("Cluster name not found, please provide an EKS (AWS) cluster name with AwsAuthRefreshable." + + "alternatively cluster name can be identified from .kube/config")) + ) + } + } + } + + // Try to parse region from cluster name + private val regionToRefresh: Regions = { + region.getOrElse { + val region: Option[String] = clusterNameToRefresh.split("/").headOption.flatMap(_.split(":").lift(4)) + region.flatMap { rg => + Try(Regions.fromName(rg)).toOption + }.getOrElse { + throw new K8SException(Status(reason = + Some("Region name not found, please provide an EKS (AWS) region name with AwsAuthRefreshable."))) + } + } + } private def refreshGcpToken(): RefreshableToken = { val token = generateAwsToken val refreshedToken = RefreshableToken(token, DateTime.now.plus(refreshInterval.toSeconds)) - refresh = Some(refreshedToken) + cachedToken = Some(refreshedToken) refreshedToken } @@ -35,7 +59,7 @@ final case class AwsAuthRefreshable(clusterName: String, val tokenService: AWSSecurityTokenServiceClient = AWSSecurityTokenServiceClientBuilder .standard() - .withRegion(region) + .withRegion(regionToRefresh) .withCredentials(credentialsProvider) .build().asInstanceOf[AWSSecurityTokenServiceClient] @@ -46,8 +70,8 @@ final case class AwsAuthRefreshable(clusterName: String, callerIdentityRequestDefaultRequest.setHttpMethod(HttpMethodName.GET) callerIdentityRequestDefaultRequest.addParameter("Action", "GetCallerIdentity") callerIdentityRequestDefaultRequest.addParameter("Version", "2011-06-15") - callerIdentityRequestDefaultRequest.addHeader("x-k8s-aws-id", clusterName) - val signer = SignerFactory.createSigner(SignerFactory.VERSION_FOUR_SIGNER, new SignerParams("sts", region.getName)) + callerIdentityRequestDefaultRequest.addHeader("x-k8s-aws-id", clusterNameToRefresh) + val signer = SignerFactory.createSigner(SignerFactory.VERSION_FOUR_SIGNER, new SignerParams("sts", regionToRefresh.getName)) val signerProvider = new DefaultSignerProvider(tokenService, signer) val presignerParams = new PresignerParams( uri, @@ -68,7 +92,7 @@ final case class AwsAuthRefreshable(clusterName: String, override def name: String = "aws" def accessToken: String = this.synchronized { - refresh match { + cachedToken match { case Some(expired) if expired.expired => refreshGcpToken().accessToken case None => @@ -82,5 +106,6 @@ final case class AwsAuthRefreshable(clusterName: String, } final case class RefreshableToken(accessToken: String, expiry: DateTime) { - def expired: Boolean = expiry.isBefore(System.currentTimeMillis) + def expired: Boolean = + expiry.isBefore(System.currentTimeMillis) } diff --git a/client/src/test/scala/skuber/api/ConfigurationSpec.scala b/client/src/test/scala/skuber/api/ConfigurationSpec.scala index 1c652f2f..72046916 100644 --- a/client/src/test/scala/skuber/api/ConfigurationSpec.scala +++ b/client/src/test/scala/skuber/api/ConfigurationSpec.scala @@ -110,9 +110,9 @@ users: // construct equivalent config directly for comparison - val cowCluster=K8SCluster("v1", "http://cow.org:8080",false) - val horseCluster=K8SCluster("v1","https://horse.org:4443", false, certificateAuthority=Some(Left("path/to/my/cafile"))) - val pigCluster=K8SCluster("v1", "https://pig.org:443", true) + val cowCluster=K8SCluster("v1", "http://cow.org:8080",false, clusterName = Some("cow-cluster")) + val horseCluster=K8SCluster("v1","https://horse.org:4443", false, certificateAuthority=Some(Left("path/to/my/cafile")), clusterName = Some("horse-cluster")) + val pigCluster=K8SCluster("v1", "https://pig.org:443", true, clusterName = Some("pig-cluster")) val clusters=Map("cow-cluster" -> cowCluster,"horse-cluster"->horseCluster,"pig-cluster"->pigCluster) val blueUser = TokenAuth("blue-token") diff --git a/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala b/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala new file mode 100644 index 00000000..cae2a606 --- /dev/null +++ b/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala @@ -0,0 +1,38 @@ +package skuber.examples.auth + +import akka.actor.ActorSystem +import skuber.api.Configuration +import skuber.api.client.token.AwsAuthRefreshable +import skuber.api.client.{Context, KubernetesClient} +import skuber.examples.customresources.CreateCRD.system +import skuber.{PodList, k8sInit} +import scala.concurrent.Await +import scala.concurrent.duration._ +import skuber.json.format._ + +class AwsAuthExample extends App { + implicit private val as = ActorSystem() + implicit private val ex = as.dispatcher + + val k8sConfigBase = Configuration() + val k8sContextCluster = k8sConfigBase.currentContext.cluster + val context = Context(cluster = k8sContextCluster, authInfo = AwsAuthRefreshable()) + val k8sConfig = k8sConfigBase.withCluster("cluster", k8sContextCluster).withContext("cluster", context).useContext(context) + + val k8s: KubernetesClient = k8sInit(k8sConfig) + + val pods = Await.result(k8s.listInNamespace[PodList]("demand"), 10.seconds) + + println(pods.items.map(_.name)) + val pods1 = Await.result(k8s.listInNamespace[PodList]("demand"), 10.seconds) + println(pods1.items.map(_.name)) + + Thread.sleep(4000) + val pods2 = Await.result(k8s.listInNamespace[PodList]("demand"), 10.seconds) + println(pods2.items.map(_.name)) + + k8s.close + as.terminate().foreach { f => + System.exit(1) + } +} From e901bef88efd61d56ba7759e0baba3a074327847 Mon Sep 17 00:00:00 2001 From: hagay3 Date: Sat, 11 Jun 2022 13:03:39 +0300 Subject: [PATCH 03/12] fix implicit --- .../src/main/scala/skuber/examples/auth/AwsAuthExample.scala | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala b/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala index cae2a606..fc109910 100644 --- a/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala +++ b/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala @@ -4,7 +4,6 @@ import akka.actor.ActorSystem import skuber.api.Configuration import skuber.api.client.token.AwsAuthRefreshable import skuber.api.client.{Context, KubernetesClient} -import skuber.examples.customresources.CreateCRD.system import skuber.{PodList, k8sInit} import scala.concurrent.Await import scala.concurrent.duration._ From 5e8eba1aa2b59af7ae8fa49df6573d96821ea213 Mon Sep 17 00:00:00 2001 From: hagay3 Date: Sun, 12 Jun 2022 23:16:40 +0300 Subject: [PATCH 04/12] remove unrelevant aws auth from config --- client/src/main/scala/skuber/api/Configuration.scala | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/client/src/main/scala/skuber/api/Configuration.scala b/client/src/main/scala/skuber/api/Configuration.scala index 1da703f0..a2fd9d74 100644 --- a/client/src/main/scala/skuber/api/Configuration.scala +++ b/client/src/main/scala/skuber/api/Configuration.scala @@ -1,6 +1,5 @@ package skuber.api -import java.io.File import java.net.URL import java.time.Instant import java.time.format.DateTimeFormatter @@ -8,11 +7,9 @@ import scala.collection.JavaConverters._ import scala.util.Try import scala.util.Failure import java.util.{Base64, Date} -import com.amazonaws.regions.Regions import org.yaml.snakeyaml.Yaml import skuber.Namespace import skuber.api.client._ -import skuber.api.client.token.AwsAuthRefreshable import scala.io.Source /** @@ -184,13 +181,6 @@ object Configuration { cmdArgs = valueAt(config, "cmd-args") ) ) - case "aws" => - Some( - AwsAuthRefreshable( - clusterName = optionalValueAt(config, "cluster-name"), - region = optionalValueAt[String](config, "region").flatMap(rg => Try(Regions.fromName(rg)).toOption) - ) - ) case _ => None } } From 094b6b4e866f142f29a2656a841e1d39c71d8459 Mon Sep 17 00:00:00 2001 From: hagay3 Date: Tue, 14 Jun 2022 12:35:50 +0300 Subject: [PATCH 05/12] refreshable token final revision --- build.sbt | 17 ++++- .../scala/skuber/api/client/Cluster.scala | 21 ++++--- .../scala/skuber/api/client/package.scala | 42 +++++++++---- .../api/client/token/AwsAuthRefreshable.scala | 62 +++++++------------ .../api/client/token/RefreshableToken.scala | 5 ++ .../skuber/examples/auth/AwsAuthExample.scala | 44 +++++++------ 6 files changed, 109 insertions(+), 82 deletions(-) create mode 100644 client/src/main/scala/skuber/api/client/token/RefreshableToken.scala diff --git a/build.sbt b/build.sbt index d74d6db7..7eef9e31 100644 --- a/build.sbt +++ b/build.sbt @@ -1,3 +1,5 @@ +import sbtassembly.AssemblyKeys.assembly +import sbtassembly.{MergeStrategy, PathList} import xerial.sbt.Sonatype._ resolvers += "Typesafe Releases" at "https://repo.typesafe.com/typesafe/releases/" @@ -56,7 +58,7 @@ ThisBuild / homepage := Some(url("https://github.com/hagay3")) publishTo := sonatypePublishToBundle.value sonatypeCredentialHost := Sonatype.sonatype01 -updateOptions in ThisBuild := updateOptions.value.withGigahorse(false) +ThisBuild / updateOptions := updateOptions.value.withGigahorse(false) sonatypeProjectHosting := Some(GitHubHosting("hagay3", "skuber", "hagay3@gmail.com")) @@ -179,7 +181,18 @@ lazy val skuber = (project in file("client")) lazy val examples = (project in file("examples")) .settings( commonSettings, + mergeStrategy, crossScalaVersions := supportedScalaVersion) .settings(examplesSettings: _*) .settings(examplesAssemblySettings: _*) - .dependsOn(skuber) \ No newline at end of file + .dependsOn(skuber) + +val mergeStrategy = Seq( + assembly / assemblyMergeStrategy := { + case PathList("module-info.class") => MergeStrategy.last + case path if path.endsWith("/module-info.class") => MergeStrategy.last + case x => + val oldStrategy = (assembly / assemblyMergeStrategy).value + oldStrategy(x) + } +) \ No newline at end of file diff --git a/client/src/main/scala/skuber/api/client/Cluster.scala b/client/src/main/scala/skuber/api/client/Cluster.scala index bcead127..61f0592e 100644 --- a/client/src/main/scala/skuber/api/client/Cluster.scala +++ b/client/src/main/scala/skuber/api/client/Cluster.scala @@ -1,14 +1,15 @@ package skuber.api.client /** - * @author David O'Riordan - * - * Defines the details needed to communicate with the API server for a Kubernetes cluster - */ + * @author David O'Riordan + * + * Defines the details needed to communicate with the API server for a Kubernetes cluster + */ case class Cluster( - apiVersion: String = "v1", - server: String = defaultApiServerURL, - insecureSkipTLSVerify: Boolean = false, - certificateAuthority: Option[PathOrData] = None, - clusterName: Option[String] = None -) + apiVersion: String = "v1", + server: String = defaultApiServerURL, + insecureSkipTLSVerify: Boolean = false, + certificateAuthority: Option[PathOrData] = None, + clusterName: Option[String] = None) { + def withName(name: String): Cluster = this.copy(clusterName = Some(name)) +} diff --git a/client/src/main/scala/skuber/api/client/package.scala b/client/src/main/scala/skuber/api/client/package.scala index d31c7adb..e940f01e 100644 --- a/client/src/main/scala/skuber/api/client/package.scala +++ b/client/src/main/scala/skuber/api/client/package.scala @@ -4,14 +4,16 @@ import java.time.Instant import java.util.UUID import akka.NotUsed import akka.actor.ActorSystem -import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.{HttpCharsets, HttpRequest, HttpResponse, MediaType} import akka.stream.scaladsl.Flow import com.typesafe.config.{Config, ConfigFactory} +import org.joda.time.DateTime import play.api.libs.functional.syntax._ import play.api.libs.json.Reads._ import play.api.libs.json._ import skuber.ObjectResource import skuber.api.client.impl.KubernetesClientImpl +import skuber.api.client.token.RefreshableToken import scala.sys.SystemProperties import scala.util.Try @@ -81,6 +83,13 @@ package object client { def name: String } + trait AuthProviderRefreshableAuth extends AuthProviderAuth { + def refreshToken: RefreshableToken + def generateToken: String + def name: String + def isTokenExpired(refreshableToken: RefreshableToken): Boolean + } + // 'jwt' supports an oidc id token per https://kubernetes.io/docs/admin/authentication/#option-1---oidc-authenticator // - but does not yet support token refresh final case class OidcAuth(idToken: String) extends AuthProviderAuth { @@ -91,24 +100,24 @@ package object client { override def toString = """OidcAuth(idToken=)""" } - final case class GcpAuth private(private val config: GcpConfiguration) extends AuthProviderAuth { + final case class GcpAuth private(private val config: GcpConfiguration) extends AuthProviderRefreshableAuth { override val name = "gcp" - @volatile private var refresh: Option[GcpRefresh] = config.cachedAccessToken.map(token => GcpRefresh(token.accessToken, token.expiry)) + @volatile private var refresh: Option[RefreshableToken] = config.cachedAccessToken.map(token => GcpRefresh(token.accessToken, token.expiry).toRefreshableToken) - private def refreshGcpToken(): GcpRefresh = { - val output = config.cmd.execute() - val parsed = Json.parse(output).as[GcpRefresh] + override def refreshToken: RefreshableToken = { + val output = generateToken + val parsed = Json.parse(output).as[GcpRefresh].toRefreshableToken refresh = Some(parsed) parsed } def accessToken: String = this.synchronized { refresh match { - case Some(expired) if expired.expired => - refreshGcpToken().accessToken + case Some(token) if isTokenExpired(token) => + refreshToken.accessToken case None => - refreshGcpToken().accessToken + refreshToken.accessToken case Some(token) => token.accessToken } @@ -116,10 +125,19 @@ package object client { override def toString = """GcpAuth(accessToken=)""".stripMargin + + override def isTokenExpired(refreshableToken: RefreshableToken): Boolean = { + DateTime.now.isAfter(refreshableToken.expiry.minusSeconds(20)) + } + override def generateToken: String = config.cmd.execute() } final private[client] case class GcpRefresh(accessToken: String, expiry: Instant) { - def expired: Boolean = Instant.now.isAfter(expiry.minusSeconds(20)) + + def toRefreshableToken: RefreshableToken = { + val expirationDate = new DateTime(this.expiry.toEpochMilli) + RefreshableToken(accessToken = this.accessToken, expiry = expirationDate) + } } private[client] object GcpRefresh { @@ -133,9 +151,7 @@ package object client { final case class GcpConfiguration(cachedAccessToken: Option[GcpCachedAccessToken], cmd: GcpCommand) - final case class GcpCachedAccessToken(accessToken: String, expiry: Instant) { - def expired: Boolean = Instant.now.isAfter(expiry.minusSeconds(20)) - } + final case class GcpCachedAccessToken(accessToken: String, expiry: Instant) final case class GcpCommand(cmd: String, args: String) { diff --git a/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala b/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala index 487387ad..2f25f4df 100644 --- a/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala +++ b/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala @@ -14,52 +14,40 @@ import org.joda.time.DateTime import skuber.K8SException import skuber.api.client._ import scala.concurrent.duration._ -import scala.util.Try -final case class AwsAuthRefreshable(clusterName: Option[String] = None, - region: Option[Regions] = None, - refreshInterval: Duration = 60.minutes) extends AuthProviderAuth { +final case class AwsAuthRefreshable(cluster: Option[Cluster] = None) extends AuthProviderRefreshableAuth { - @volatile private var cachedToken: Option[RefreshableToken] = None + @volatile var cachedToken: Option[RefreshableToken] = None - private val clusterNameToRefresh: String = { - clusterName.getOrElse { + private val clusterName: String = { + cluster.flatMap(_.clusterName).getOrElse { defaultK8sConfig.currentContext.cluster.clusterName.getOrElse { throw new K8SException(Status(reason = - Some("Cluster name not found, please provide an EKS (AWS) cluster name with AwsAuthRefreshable." + - "alternatively cluster name can be identified from .kube/config")) - ) + Some("Cluster name not found, please provide an EKS (AWS) cluster name within Cluster object"))) } } } - // Try to parse region from cluster name - private val regionToRefresh: Regions = { - region.getOrElse { - val region: Option[String] = clusterNameToRefresh.split("/").headOption.flatMap(_.split(":").lift(4)) - region.flatMap { rg => - Try(Regions.fromName(rg)).toOption - }.getOrElse { - throw new K8SException(Status(reason = - Some("Region name not found, please provide an EKS (AWS) region name with AwsAuthRefreshable."))) - } - } - } + // https://github.com/kubernetes-sigs/aws-iam-authenticator/blob/27337b2b74c3140cf745a64f7154fe8ff7592258/pkg/token/token.go#L87 + // STS provides 15 minutes expiration, and it's not configurable. + // Using 10 minutes to be on the safe side. + private val refreshInterval = 10.minutes - private def refreshGcpToken(): RefreshableToken = { - val token = generateAwsToken - val refreshedToken = RefreshableToken(token, DateTime.now.plus(refreshInterval.toSeconds)) + override def refreshToken: RefreshableToken = { + val token = generateToken + val refreshedToken = RefreshableToken(token, DateTime.now.plus(refreshInterval.toMillis)) cachedToken = Some(refreshedToken) refreshedToken } - private def generateAwsToken: String = { + override def generateToken: String = { try { val credentialsProvider = new AWSStaticCredentialsProvider(new DefaultAWSCredentialsProviderChain().getCredentials) + val signer: Signer = SignerFactory.createSigner(SignerFactory.VERSION_FOUR_SIGNER, + new SignerParams("sts", Regions.US_EAST_1.getName)) //it needs to be "us_east_1" val tokenService: AWSSecurityTokenServiceClient = AWSSecurityTokenServiceClientBuilder .standard() - .withRegion(regionToRefresh) .withCredentials(credentialsProvider) .build().asInstanceOf[AWSSecurityTokenServiceClient] @@ -70,19 +58,20 @@ final case class AwsAuthRefreshable(clusterName: Option[String] = None, callerIdentityRequestDefaultRequest.setHttpMethod(HttpMethodName.GET) callerIdentityRequestDefaultRequest.addParameter("Action", "GetCallerIdentity") callerIdentityRequestDefaultRequest.addParameter("Version", "2011-06-15") - callerIdentityRequestDefaultRequest.addHeader("x-k8s-aws-id", clusterNameToRefresh) - val signer = SignerFactory.createSigner(SignerFactory.VERSION_FOUR_SIGNER, new SignerParams("sts", regionToRefresh.getName)) + callerIdentityRequestDefaultRequest.addHeader("x-k8s-aws-id", clusterName) + val signerProvider = new DefaultSignerProvider(tokenService, signer) val presignerParams = new PresignerParams( uri, credentialsProvider, signerProvider, SdkClock.STANDARD) + val presignerFacade = new PresignerFacade(presignerParams) val url = presignerFacade.presign(callerIdentityRequestDefaultRequest, new Date()) val encodedUrl = Base64.getUrlEncoder.withoutPadding().encodeToString(url.toString.getBytes) - s"k8s-aws-v1.$encodedUrl" + } catch { case e: Exception => throw new K8SException(Status(reason = Option(e.getMessage))) @@ -91,21 +80,18 @@ final case class AwsAuthRefreshable(clusterName: Option[String] = None, override def name: String = "aws" - def accessToken: String = this.synchronized { + override def accessToken: String = this.synchronized { cachedToken match { - case Some(expired) if expired.expired => - refreshGcpToken().accessToken + case Some(token) if isTokenExpired(token) => + refreshToken.accessToken case None => - refreshGcpToken().accessToken + refreshToken.accessToken case Some(token) => token.accessToken } } override def toString: String = """AwsAuth(accessToken=)""".stripMargin -} -final case class RefreshableToken(accessToken: String, expiry: DateTime) { - def expired: Boolean = - expiry.isBefore(System.currentTimeMillis) + override def isTokenExpired(refreshableToken: RefreshableToken): Boolean = refreshableToken.expiry.isBefore(System.currentTimeMillis) } diff --git a/client/src/main/scala/skuber/api/client/token/RefreshableToken.scala b/client/src/main/scala/skuber/api/client/token/RefreshableToken.scala new file mode 100644 index 00000000..a51c1c0c --- /dev/null +++ b/client/src/main/scala/skuber/api/client/token/RefreshableToken.scala @@ -0,0 +1,5 @@ +package skuber.api.client.token + +import org.joda.time.DateTime + +final case class RefreshableToken(accessToken: String, expiry: DateTime) diff --git a/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala b/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala index fc109910..97635999 100644 --- a/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala +++ b/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala @@ -1,37 +1,43 @@ package skuber.examples.auth +import java.util.Base64 import akka.actor.ActorSystem +import org.joda.time.DateTime import skuber.api.Configuration import skuber.api.client.token.AwsAuthRefreshable -import skuber.api.client.{Context, KubernetesClient} +import skuber.api.client.{Cluster, Context, KubernetesClient} +import skuber.json.format._ import skuber.{PodList, k8sInit} import scala.concurrent.Await import scala.concurrent.duration._ -import skuber.json.format._ -class AwsAuthExample extends App { +object AwsAuthExample extends App { implicit private val as = ActorSystem() implicit private val ex = as.dispatcher + val namespace = System.getenv("namespace") + val serverUrl = System.getenv("serverUrl") + val certificate = Base64.getDecoder.decode(System.getenv("certificate")) + val clusterName = System.getenv("clusterName") + val cluster = Cluster(server = serverUrl, certificateAuthority = Some(Right(certificate)), clusterName = Some(clusterName)) - val k8sConfigBase = Configuration() - val k8sContextCluster = k8sConfigBase.currentContext.cluster - val context = Context(cluster = k8sContextCluster, authInfo = AwsAuthRefreshable()) - val k8sConfig = k8sConfigBase.withCluster("cluster", k8sContextCluster).withContext("cluster", context).useContext(context) - - val k8s: KubernetesClient = k8sInit(k8sConfig) - - val pods = Await.result(k8s.listInNamespace[PodList]("demand"), 10.seconds) + val context = Context(cluster = cluster, authInfo = AwsAuthRefreshable(cluster = Some(cluster))) - println(pods.items.map(_.name)) - val pods1 = Await.result(k8s.listInNamespace[PodList]("demand"), 10.seconds) - println(pods1.items.map(_.name)) + val k8sConfig = Configuration(clusters = Map(clusterName -> cluster), contexts = Map(clusterName -> context)).useContext(context) - Thread.sleep(4000) - val pods2 = Await.result(k8s.listInNamespace[PodList]("demand"), 10.seconds) - println(pods2.items.map(_.name)) + val k8s: KubernetesClient = k8sInit(k8sConfig) + listPods(namespace, 0) + listPods(namespace, 5) + listPods(namespace, 11) k8s.close - as.terminate().foreach { f => - System.exit(1) + Await.result(as.terminate(), 10.seconds) + System.exit(1) + + def listPods(namespace: String, minutesSleep: Int): Unit = { + println(s"Sleeping $minutesSleep minutes...") + Thread.sleep(minutesSleep * 60 * 1000) + println(DateTime.now) + val pods = Await.result(k8s.listInNamespace[PodList](namespace), 10.seconds) + println(pods.items.map(_.name)) } } From 7104afb13cf6f3a9ebdea0b613bffd6eb73aac61 Mon Sep 17 00:00:00 2001 From: hagay3 Date: Wed, 15 Jun 2022 18:36:22 +0300 Subject: [PATCH 06/12] add regional sts to aws auth refreshable token --- client/src/main/scala/skuber/api/client/Cluster.scala | 8 +++++++- .../skuber/api/client/token/AwsAuthRefreshable.scala | 9 ++++++++- .../main/scala/skuber/examples/auth/AwsAuthExample.scala | 4 +++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/client/src/main/scala/skuber/api/client/Cluster.scala b/client/src/main/scala/skuber/api/client/Cluster.scala index 61f0592e..d29672c4 100644 --- a/client/src/main/scala/skuber/api/client/Cluster.scala +++ b/client/src/main/scala/skuber/api/client/Cluster.scala @@ -1,5 +1,7 @@ package skuber.api.client +import com.amazonaws.regions.Regions + /** * @author David O'Riordan * @@ -10,6 +12,10 @@ case class Cluster( server: String = defaultApiServerURL, insecureSkipTLSVerify: Boolean = false, certificateAuthority: Option[PathOrData] = None, - clusterName: Option[String] = None) { + clusterName: Option[String] = None, + awsRegion: Option[Regions] = None + ) { def withName(name: String): Cluster = this.copy(clusterName = Some(name)) + + def withAwsRegion(region: Regions): Cluster = this.copy(awsRegion = Some(region)) } diff --git a/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala b/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala index 2f25f4df..ee1ee1cc 100644 --- a/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala +++ b/client/src/main/scala/skuber/api/client/token/AwsAuthRefreshable.scala @@ -28,6 +28,9 @@ final case class AwsAuthRefreshable(cluster: Option[Cluster] = None) extends Aut } } + private val region: Option[Regions] = cluster.map(_.awsRegion).getOrElse(defaultK8sConfig.currentContext.cluster.awsRegion) + + // https://github.com/kubernetes-sigs/aws-iam-authenticator/blob/27337b2b74c3140cf745a64f7154fe8ff7592258/pkg/token/token.go#L87 // STS provides 15 minutes expiration, and it's not configurable. // Using 10 minutes to be on the safe side. @@ -52,7 +55,11 @@ final case class AwsAuthRefreshable(cluster: Option[Cluster] = None) extends Aut .build().asInstanceOf[AWSSecurityTokenServiceClient] val callerIdentityRequestDefaultRequest = new DefaultRequest[GetCallerIdentityRequest](new GetCallerIdentityRequest(), "sts") - val uri = new URI("https", "sts.amazonaws.com", null, null) + + val regionStr = region.map(rg => s".${rg.getName}").getOrElse("") + val stsHost = s"sts$regionStr.amazonaws.com" + + val uri = new URI("https", stsHost, null, null) callerIdentityRequestDefaultRequest.setResourcePath("/") callerIdentityRequestDefaultRequest.setEndpoint(uri) callerIdentityRequestDefaultRequest.setHttpMethod(HttpMethodName.GET) diff --git a/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala b/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala index 97635999..c1098957 100644 --- a/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala +++ b/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala @@ -2,6 +2,7 @@ package skuber.examples.auth import java.util.Base64 import akka.actor.ActorSystem +import com.amazonaws.regions.Regions import org.joda.time.DateTime import skuber.api.Configuration import skuber.api.client.token.AwsAuthRefreshable @@ -18,7 +19,8 @@ object AwsAuthExample extends App { val serverUrl = System.getenv("serverUrl") val certificate = Base64.getDecoder.decode(System.getenv("certificate")) val clusterName = System.getenv("clusterName") - val cluster = Cluster(server = serverUrl, certificateAuthority = Some(Right(certificate)), clusterName = Some(clusterName)) + val region = Regions.fromName(System.getenv("region")) + val cluster = Cluster(server = serverUrl, certificateAuthority = Some(Right(certificate)), clusterName = Some(clusterName), awsRegion = Some(region)) val context = Context(cluster = cluster, authInfo = AwsAuthRefreshable(cluster = Some(cluster))) From be2d894a09fe1d708e018865926e6248469e0b36 Mon Sep 17 00:00:00 2001 From: hagay3 Date: Thu, 23 Jun 2022 21:48:40 +0300 Subject: [PATCH 07/12] add readme file --- README.md | 16 ++-- docs/Refresh_EKS_AWS_Token.md | 169 ++++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 8 deletions(-) create mode 100644 docs/Refresh_EKS_AWS_Token.md diff --git a/README.md b/README.md index a10541f1..6d80fe79 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ Skuber is a Scala client library for [Kubernetes](http://kubernetes.io). It prov ## Features - +- Uses standard `kubeconfig` files for configuration - see the [configuration guide](docs/Configuration.md) for details +- Refreshing EKS tokens [Refresh EKS Token guide](docs/Refresh_EKS_AWS_Token.md) - Comprehensive support for Kubernetes API model represented as Scala case classes - Support for core, extensions and other Kubernetes API groups - Full support for converting resources between the case class and standard JSON representations - Client API for creating, reading, updating, removing, listing and watching resources on a Kubernetes cluster - The API is asynchronous and strongly typed e.g. `k8s get[Deployment]("nginx")` returns a value of type `Future[Deployment]` - Fluent API for creating and updating specifications of Kubernetes resources -- Uses standard `kubeconfig` files for configuration - see the [configuration guide](docs/Configuration.md) for details See the [programming guide](docs/GUIDE.md) for more details. @@ -147,18 +147,18 @@ ci.yaml and clean.yaml are generated automatically with [sbt-github-actions](htt Run `sbt githubWorkflowGenerate && bash infra/ci/fix-workflows.sh` in order to regenerate ci.yaml and clean.yaml. CI Running against the following k8s versions - -skuber supports all other k8s versions, not all of them tested under CI. - -https://kubernetes.io/releases/ - -* v1.19.6 +* v1.19.6 * v1.20.11 * v1.21.5 * v1.22.9 * v1.23.6 * v1.24.1 +skuber supports all other k8s versions, not all of them tested under CI. + +https://kubernetes.io/releases/ + + ## License diff --git a/docs/Refresh_EKS_AWS_Token.md b/docs/Refresh_EKS_AWS_Token.md new file mode 100644 index 00000000..bfc21953 --- /dev/null +++ b/docs/Refresh_EKS_AWS_Token.md @@ -0,0 +1,169 @@ +# Refresh EKS (AWS) Token + +### Background +Skuber has the functionality to refresh your EKS (AWS) token with an IAM role and cluster configurations. + +The initiative: +* Refreshing tokens increasing your k8s cluster security +* Since kubernetes v1.21 service account tokens has an expiration, + which means that you must use this feature with: + skuber + EKS + k8s v1.21+. + https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.21 + + +## Step by step guide +Pay attention to the fact that skuber can be deployed in one cluster and the cluster you want to control can be another cluster.
+In this guide I will use the following: + +`SKUBER_CLUSTER` - the cluster skuber app will bed deployed on.
+`TARGET_CLUSTER` - the cluster that skuber will be connected to. + +### Setup the environment variables +* Make sure aws cli is configured properly + +``` +export ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account) +echo $ACCOUNT_ID +``` +Set the cluster name and region you need access to (`TARGET_CLUSTER`)
+use `aws eks list-clusters` + +``` +export TARGET_CLUSTER=example-cluster +export REGION=us-east-1 +``` + + +Set the cluster name which skuber app will run from (`SKUBER_CLUSTER`) + +Set the namespace name which skuber app will run from + +Set the oidc provider id + +Set the service account name that skuber app will be attached to + +``` +export SKUBER_CLUSTER=skuber-cluster +export SKUBER_NAMESPACE=skuber-namespace +export OIDC=$(aws eks describe-cluster --name $SKUBER_CLUSTER --output text --query cluster.identity.oidc.issuer | cut -d'/' -f3,4,5) +echo $OIDC +export SKUBER_SA=skuber-serviceaccount +``` + +### Create a file with the IAM policy and federated principal + +This role will map the service account that skuber uses to the cluster it connects to.
+You can add more clusters under "Resource" +* Note: eks actions probably need to be minimized and not set to "eks:*" .
+``` +cat > skuber_iam_role.json < Kubernetes Permissions + +For this example I'm using existing masters permissions group, you can create something more specific with [RBAC](https://docs.aws.amazon.com/eks/latest/userguide/add-user-role.html). +* You need to create this mapping on every cluster that you want skuber will be able to interact with. + +Change the context to `TARGET_CLUSTER`. +``` +kubectl config use-context arn:aws:eks:${REGION}:${ACCOUNT_ID}:cluster/${TARGET_CLUSTER} +kubectl edit configmap aws-auth -n kube-system +``` + +Add the following mapping +* Replace the variables with the actual value +``` + - rolearn: arn:aws:iam::$ACCOUNT_ID:role/$IAM_ROLE_NAME + username: ci + groups: + - system:masters +``` + + +### Skuber Code example + +``` +implicit private val as = ActorSystem() + implicit private val ex = as.dispatcher + val namespace = System.getenv("namespace") + val serverUrl = System.getenv("serverUrl") + val certificate = Base64.getDecoder.decode(System.getenv("certificate")) + val clusterName = System.getenv("clusterName") + val region = Regions.fromName(System.getenv("region")) + val cluster = Cluster(server = serverUrl, certificateAuthority = Some(Right(certificate)), clusterName = Some(clusterName), awsRegion = Some(region)) + + val context = Context(cluster = cluster, authInfo = AwsAuthRefreshable(cluster = Some(cluster))) + + val k8sConfig = Configuration(clusters = Map(clusterName -> cluster), contexts = Map(clusterName -> context)).useContext(context) + + val k8s: KubernetesClient = k8sInit(k8sConfig) + listPods(namespace, 0) + listPods(namespace, 5) + listPods(namespace, 11) + + k8s.close + Await.result(as.terminate(), 10.seconds) + System.exit(1) + + def listPods(namespace: String, minutesSleep: Int): Unit = { + println(s"Sleeping $minutesSleep minutes...") + Thread.sleep(minutesSleep * 60 * 1000) + println(DateTime.now) + val pods = Await.result(k8s.listInNamespace[PodList](namespace), 10.seconds) + println(pods.items.map(_.name)) + } +``` From a7886cf60e99a9937cedb37521c1ce912ecb81a3 Mon Sep 17 00:00:00 2001 From: hagay3 Date: Thu, 23 Jun 2022 21:56:16 +0300 Subject: [PATCH 08/12] improvements to aws readme --- docs/Refresh_EKS_AWS_Token.md | 16 ++++++++-------- .../skuber/examples/auth/AwsAuthExample.scala | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/Refresh_EKS_AWS_Token.md b/docs/Refresh_EKS_AWS_Token.md index bfc21953..350a8486 100644 --- a/docs/Refresh_EKS_AWS_Token.md +++ b/docs/Refresh_EKS_AWS_Token.md @@ -15,7 +15,7 @@ The initiative: Pay attention to the fact that skuber can be deployed in one cluster and the cluster you want to control can be another cluster.
In this guide I will use the following: -`SKUBER_CLUSTER` - the cluster skuber app will bed deployed on.
+`SKUBER_CLUSTER` - the cluster skuber app will be deployed on.
`TARGET_CLUSTER` - the cluster that skuber will be connected to. ### Setup the environment variables @@ -26,7 +26,7 @@ export ACCOUNT_ID=$(aws sts get-caller-identity --output text --query Account) echo $ACCOUNT_ID ``` Set the cluster name and region you need access to (`TARGET_CLUSTER`)
-use `aws eks list-clusters` +use `aws eks list-clusters` to see the cluster names. ``` export TARGET_CLUSTER=example-cluster @@ -40,7 +40,7 @@ Set the namespace name which skuber app will run from Set the oidc provider id -Set the service account name that skuber app will be attached to +Set the service account name that skuber app will be attached to (we will create it later) ``` export SKUBER_CLUSTER=skuber-cluster @@ -98,8 +98,8 @@ aws iam create-role \ ``` ### Create a service account -Change the context to `TARGET_CLUSTER`. -Create the service account in `SKUBER_CLUSTER` +Change the context to `SKUBER_CLUSTER` and create the service account
+ ``` kubectl config use-context arn:aws:eks:${REGION}:${ACCOUNT_ID}:cluster/${SKUBER_CLUSTER} @@ -125,7 +125,7 @@ kubectl edit configmap aws-auth -n kube-system ``` Add the following mapping -* Replace the variables with the actual value +* Replace the variables with the actual values ``` - rolearn: arn:aws:iam::$ACCOUNT_ID:role/$IAM_ROLE_NAME username: ci @@ -135,7 +135,7 @@ Add the following mapping ### Skuber Code example - +A working example for using `AwsAuthRefreshable` ``` implicit private val as = ActorSystem() implicit private val ex = as.dispatcher @@ -157,7 +157,7 @@ implicit private val as = ActorSystem() k8s.close Await.result(as.terminate(), 10.seconds) - System.exit(1) + System.exit(0) def listPods(namespace: String, minutesSleep: Int): Unit = { println(s"Sleeping $minutesSleep minutes...") diff --git a/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala b/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala index c1098957..76e8c217 100644 --- a/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala +++ b/examples/src/main/scala/skuber/examples/auth/AwsAuthExample.scala @@ -33,7 +33,7 @@ object AwsAuthExample extends App { k8s.close Await.result(as.terminate(), 10.seconds) - System.exit(1) + System.exit(0) def listPods(namespace: String, minutesSleep: Int): Unit = { println(s"Sleeping $minutesSleep minutes...") From f7179b37283370f3c27b722643f1cd7fc7ccc8fd Mon Sep 17 00:00:00 2001 From: hagay3 Date: Thu, 23 Jun 2022 21:57:56 +0300 Subject: [PATCH 09/12] improvements to aws readme --- docs/Refresh_EKS_AWS_Token.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/docs/Refresh_EKS_AWS_Token.md b/docs/Refresh_EKS_AWS_Token.md index 350a8486..6480e754 100644 --- a/docs/Refresh_EKS_AWS_Token.md +++ b/docs/Refresh_EKS_AWS_Token.md @@ -1,13 +1,11 @@ # Refresh EKS (AWS) Token -### Background +## Background Skuber has the functionality to refresh your EKS (AWS) token with an IAM role and cluster configurations. The initiative: * Refreshing tokens increasing your k8s cluster security -* Since kubernetes v1.21 service account tokens has an expiration, - which means that you must use this feature with: - skuber + EKS + k8s v1.21+. +* Since kubernetes v1.21 service account tokens has an expiration of 1 hour. https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.21 From 1f80f8443c194b464b4038c049503264bfcf0b32 Mon Sep 17 00:00:00 2001 From: hagay3 Date: Thu, 23 Jun 2022 21:58:09 +0300 Subject: [PATCH 10/12] improvements to aws readme --- docs/Refresh_EKS_AWS_Token.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Refresh_EKS_AWS_Token.md b/docs/Refresh_EKS_AWS_Token.md index 6480e754..4a3956b9 100644 --- a/docs/Refresh_EKS_AWS_Token.md +++ b/docs/Refresh_EKS_AWS_Token.md @@ -9,7 +9,7 @@ The initiative: https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#kubernetes-1.21 -## Step by step guide +## Step-by-step guide Pay attention to the fact that skuber can be deployed in one cluster and the cluster you want to control can be another cluster.
In this guide I will use the following: From a673bc865b23b10dcdf15ba3027457f63536ee99 Mon Sep 17 00:00:00 2001 From: hagay3 Date: Thu, 23 Jun 2022 22:08:16 +0300 Subject: [PATCH 11/12] improvements to aws readme --- docs/Refresh_EKS_AWS_Token.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/docs/Refresh_EKS_AWS_Token.md b/docs/Refresh_EKS_AWS_Token.md index 4a3956b9..2d3ad661 100644 --- a/docs/Refresh_EKS_AWS_Token.md +++ b/docs/Refresh_EKS_AWS_Token.md @@ -1,5 +1,13 @@ # Refresh EKS (AWS) Token +[Background](#background) +[Step-by-step guide](#step-by-step-guide) +[Setup the environment variables](#setup-the-environment-variables) +[Create IAM Role](#create-iam-role) +[Create a service account](#create-a-service-account) +[Create the aws-auth mapping](#create-the-aws-auth-mapping) +[Skuber Code example](#skuber-code-example) + ## Background Skuber has the functionality to refresh your EKS (AWS) token with an IAM role and cluster configurations. @@ -48,7 +56,7 @@ echo $OIDC export SKUBER_SA=skuber-serviceaccount ``` -### Create a file with the IAM policy and federated principal +### Create IAM Role This role will map the service account that skuber uses to the cluster it connects to.
You can add more clusters under "Resource" @@ -83,7 +91,6 @@ cat > skuber_iam_role.json < Date: Thu, 23 Jun 2022 22:10:54 +0300 Subject: [PATCH 12/12] improvements to aws readme --- docs/Refresh_EKS_AWS_Token.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/Refresh_EKS_AWS_Token.md b/docs/Refresh_EKS_AWS_Token.md index 2d3ad661..ba88f0cf 100644 --- a/docs/Refresh_EKS_AWS_Token.md +++ b/docs/Refresh_EKS_AWS_Token.md @@ -1,12 +1,12 @@ # Refresh EKS (AWS) Token -[Background](#background) -[Step-by-step guide](#step-by-step-guide) -[Setup the environment variables](#setup-the-environment-variables) -[Create IAM Role](#create-iam-role) -[Create a service account](#create-a-service-account) -[Create the aws-auth mapping](#create-the-aws-auth-mapping) -[Skuber Code example](#skuber-code-example) +[Background](#background)
+[Step-by-step guide](#step-by-step-guide)
+[Setup the environment variables](#setup-the-environment-variables)
+[Create IAM Role](#create-iam-role)
+[Create a service account](#create-a-service-account)
+[Create the aws-auth mapping](#create-the-aws-auth-mapping)
+[Skuber Code example](#skuber-code-example) ## Background Skuber has the functionality to refresh your EKS (AWS) token with an IAM role and cluster configurations.