From 9e484d99aea9f7da43a713586adc2aa4dbe3bbe1 Mon Sep 17 00:00:00 2001 From: James Roper Date: Wed, 23 Oct 2019 21:50:03 +1100 Subject: [PATCH] More GCP fixes * Don't require either the access token or expiry fields to be present in gcp auth. * Ignore expiry dates that can't be parsed. --- .../main/scala/skuber/api/Configuration.scala | 15 ++++---- .../scala/skuber/api/client/package.scala | 38 +++++++++++++------ .../scala/skuber/api/ConfigurationSpec.scala | 20 ++++++++-- .../test/scala/skuber/model/AuthSpec.scala | 2 +- 4 files changed, 52 insertions(+), 23 deletions(-) diff --git a/client/src/main/scala/skuber/api/Configuration.scala b/client/src/main/scala/skuber/api/Configuration.scala index aefee758..e5bbbd06 100644 --- a/client/src/main/scala/skuber/api/Configuration.scala +++ b/client/src/main/scala/skuber/api/Configuration.scala @@ -115,12 +115,11 @@ object Configuration { def valueAt[T](parent: YamlMap, key: String, fallback: Option[T] = None) : T = parent.asScala.get(key).orElse(fallback).get.asInstanceOf[T] - def instantValueAt[T](parent: YamlMap, key: String) : Instant = - parent.asScala.get(key) match { - case Some(d: Date) => d.toInstant - case Some(s: String) => parseInstant(s) - case Some(unsupported) => sys.error(s"Unsupported date type: $unsupported") - case None => sys.error(s"No value found at $key") + def optionalInstantValueAt[T](parent: YamlMap, key: String) : Option[Instant] = + parent.asScala.get(key).flatMap { + case d: Date => Some(d.toInstant) + case s: String => Try(parseInstant(s)).toOption + case _ => None } def optionalValueAt[T](parent: YamlMap, key: String) : Option[T] = @@ -175,8 +174,8 @@ object Configuration { case "gcp" => Some( GcpAuth( - accessToken = valueAt(config, "access-token"), - expiry = instantValueAt(config, "expiry"), + accessToken = optionalValueAt(config, "access-token"), + expiry = optionalInstantValueAt(config, "expiry"), cmdPath = valueAt(config, "cmd-path"), cmdArgs = valueAt(config, "cmd-args") ) diff --git a/client/src/main/scala/skuber/api/client/package.scala b/client/src/main/scala/skuber/api/client/package.scala index 9069ed9d..56d6d23b 100644 --- a/client/src/main/scala/skuber/api/client/package.scala +++ b/client/src/main/scala/skuber/api/client/package.scala @@ -97,18 +97,24 @@ package object client { final case class GcpAuth private(private val config: GcpConfiguration) extends AuthProviderAuth { override val name = "gcp" - @volatile private var refresh: GcpRefresh = new GcpRefresh(config.accessToken, config.expiry) + @volatile private var refresh: Option[GcpRefresh] = config.cachedAccessToken.map(token => GcpRefresh(token.accessToken, token.expiry)) - def refreshGcpToken(): GcpRefresh = { + private def refreshGcpToken(): GcpRefresh = { val output = config.cmd.execute() - Json.parse(output).as[GcpRefresh] + val parsed = Json.parse(output).as[GcpRefresh] + refresh = Some(parsed) + parsed } def accessToken: String = this.synchronized { - if (refresh.expired) - refresh = refreshGcpToken() - - refresh.accessToken + refresh match { + case Some(expired) if expired.expired => + refreshGcpToken().accessToken + case None => + refreshGcpToken().accessToken + case Some(token) => + token.accessToken + } } override def toString = @@ -120,13 +126,19 @@ package object client { } private[client] object GcpRefresh { + // todo - the path to read this from is part of the configuration, use that instead of + // hard coding. implicit val gcpRefreshReads: Reads[GcpRefresh] = ( (JsPath \ "credential" \ "access_token").read[String] and (JsPath \ "credential" \ "token_expiry").read[Instant] ) (GcpRefresh.apply _) } - final case class GcpConfiguration(accessToken: String, expiry: Instant, cmd: GcpCommand) + 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 GcpCommand(cmd: String, args: String) { @@ -136,14 +148,18 @@ package object client { } object GcpAuth { - def apply(accessToken: String, expiry: Instant, cmdPath: String, cmdArgs: String): GcpAuth = + def apply(accessToken: Option[String], expiry: Option[Instant], cmdPath: String, cmdArgs: String): GcpAuth = { + val cachedAccessToken = for { + token <- accessToken + exp <- expiry + } yield GcpCachedAccessToken(token, exp) new GcpAuth( GcpConfiguration( - accessToken = accessToken, - expiry = expiry, + cachedAccessToken = cachedAccessToken, GcpCommand(cmdPath, cmdArgs) ) ) + } } // for use with the Watch command diff --git a/client/src/test/scala/skuber/api/ConfigurationSpec.scala b/client/src/test/scala/skuber/api/ConfigurationSpec.scala index ccd5964c..2c793d18 100644 --- a/client/src/test/scala/skuber/api/ConfigurationSpec.scala +++ b/client/src/test/scala/skuber/api/ConfigurationSpec.scala @@ -3,7 +3,8 @@ package skuber.api import skuber._ import org.specs2.mutable.Specification import java.nio.file.Paths -import java.time.Instant +import java.time.format.DateTimeFormatter +import java.time.{Instant, ZoneId} import akka.stream.ActorMaterializer import akka.actor.ActorSystem @@ -88,6 +89,16 @@ users: expiry-key: '{.credential.token_expiry}' token-key: '{.credential.access_token}' name: gcp +- name: other-date-gke-user + user: + auth-provider: + config: + cmd-args: config config-helper --format=json + cmd-path: /home/user/google-cloud-sdk/bin/gcloud + expiry: "2018-03-04 14:08:18" + expiry-key: '{.credential.token_expiry}' + token-key: '{.credential.access_token}' + name: gcp """ implicit val system=ActorSystem("test") @@ -109,9 +120,12 @@ users: val blueUser = TokenAuth("blue-token") val greenUser = CertAuth(clientCertificate = Left("path/to/my/client/cert"), clientKey = Left("path/to/my/client/key"), user = None) val jwtUser= OidcAuth(idToken = "jwt-token") - val gcpUser = GcpAuth(accessToken = "myAccessToken", expiry = Instant.parse("2018-03-04T14:08:18Z"), + val gcpUser = GcpAuth(accessToken = Some("myAccessToken"), expiry = Some(Instant.parse("2018-03-04T14:08:18Z")), + cmdPath = "/home/user/google-cloud-sdk/bin/gcloud", cmdArgs = "config config-helper --format=json") + val noAccessTokenGcpUser = GcpAuth(accessToken = None, expiry = None, cmdPath = "/home/user/google-cloud-sdk/bin/gcloud", cmdArgs = "config config-helper --format=json") - val users=Map("blue-user"->blueUser,"green-user"->greenUser,"jwt-user"->jwtUser, "gke-user"->gcpUser, "string-date-gke-user"->gcpUser) + val users=Map("blue-user"->blueUser,"green-user"->greenUser,"jwt-user"->jwtUser, "gke-user"->gcpUser, + "string-date-gke-user"->gcpUser, "other-date-gke-user" -> noAccessTokenGcpUser) val federalContext=K8SContext(horseCluster,greenUser,Namespace.forName("chisel-ns")) val queenAnneContext=K8SContext(pigCluster,blueUser, Namespace.forName("saw-ns")) diff --git a/client/src/test/scala/skuber/model/AuthSpec.scala b/client/src/test/scala/skuber/model/AuthSpec.scala index 02d42d54..ecdcd5fa 100644 --- a/client/src/test/scala/skuber/model/AuthSpec.scala +++ b/client/src/test/scala/skuber/model/AuthSpec.scala @@ -41,7 +41,7 @@ class AuthSpec extends Specification { } "GcpAuth toString masks accessToken" >> { - GcpAuth(accessToken = "MyAccessToken", expiry = Instant.now, cmdPath = "gcp", cmdArgs = "").toString mustEqual + GcpAuth(accessToken = Some("MyAccessToken"), expiry = Some(Instant.now), cmdPath = "gcp", cmdArgs = "").toString mustEqual "GcpAuth(accessToken=)" }