From 1837af8c608c5fab36715b2b91b08b422ec5353d Mon Sep 17 00:00:00 2001 From: Levi Ramsey Date: Thu, 25 Jul 2024 11:11:14 -0400 Subject: [PATCH] feat: allow configurable choice of annotations for rolling update revision --- .gitignore | 3 ++ .../src/main/resources/reference.conf | 5 +++ .../kubernetes/KubernetesApiImpl.scala | 5 ++- .../kubernetes/KubernetesJsonSupport.scala | 39 ++++++++++++++++++- .../kubernetes/KubernetesSettings.scala | 18 +++++++-- 5 files changed, 64 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index cc2ca53ee..4203b455a 100755 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,6 @@ target/ # attachments created by scripts/prepare-downloads.sh docs/src/main/paradox/attachments + +# Metals detritus +project/project diff --git a/rolling-update-kubernetes/src/main/resources/reference.conf b/rolling-update-kubernetes/src/main/resources/reference.conf index 02afd824e..134a7b223 100644 --- a/rolling-update-kubernetes/src/main/resources/reference.conf +++ b/rolling-update-kubernetes/src/main/resources/reference.conf @@ -34,6 +34,11 @@ akka.rollingupdate.kubernetes { pod-name = "" pod-name = ${?KUBERNETES_POD_NAME} + # Annotations to check to determine the revision. The first one in the list which matches + # wins. The default is suitable for "vanilla" Kubernetes Deployments, but other CI/CD systems + # may set a different annotation. + revision-annotation = [ "deployment.kubernetes.io/revision" ] + secure-api-server = true # Configuration for the Pod Deletion Cost extension diff --git a/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesApiImpl.scala b/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesApiImpl.scala index 32d9d02a9..4860f0028 100644 --- a/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesApiImpl.scala +++ b/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesApiImpl.scala @@ -55,6 +55,8 @@ import scala.util.control.NonFatal import system.dispatcher + override val _revisionAnnotations = settings + private implicit val sys: ActorSystem = system private val log = Logging(system, classOf[KubernetesApiImpl]) private val http = Http()(system) @@ -93,6 +95,7 @@ import scala.util.control.NonFatal case HttpResponse(status, _, e, _) => e.discardBytes() throw new PodCostException(s"Request failed with status=$status") + // Can we make this exhaustive? } } @@ -213,7 +216,7 @@ PUTs must contain resourceVersions. Response: case None => Future.failed(new ReadRevisionException("No replica name found")) } - val revision = replicaSet.map(_.metadata.annotations.`deployment.kubernetes.io/revision`) + val revision = replicaSet.map(_.metadata.annotations.revision) revision.map(Some(_)).recoverWith { case ex => if (tries >= maxTries) { diff --git a/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesJsonSupport.scala b/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesJsonSupport.scala index cb929c3e8..64fdbcc19 100644 --- a/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesJsonSupport.scala +++ b/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesJsonSupport.scala @@ -10,13 +10,16 @@ import akka.annotation.InternalApi import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport import spray.json.DefaultJsonProtocol import spray.json.JsonFormat +import spray.json.JsObject import spray.json.RootJsonFormat +import spray.json.JsString +import spray.json.JsValue /** * INTERNAL API */ @InternalApi -case class ReplicaAnnotation(`deployment.kubernetes.io/revision`: String) +case class ReplicaAnnotation(revision: String, source: String, otherAnnotations: Map[String, JsValue]) /** * INTERNAL API @@ -75,6 +78,10 @@ case class Spec(pods: immutable.Seq[PodCost]) */ @InternalApi trait KubernetesJsonSupport extends SprayJsonSupport with DefaultJsonProtocol { + val _revisionAnnotations: HasRevisionAnnotations = new HasRevisionAnnotations { + def revisionAnnotations = Seq("deployment.kubernetes.io/revision") + } + // If adding more formats here, remember to also add in META-INF/native-image reflect config implicit val metadataFormat: JsonFormat[Metadata] = jsonFormat2(Metadata.apply) implicit val podCostFormat: JsonFormat[PodCost] = jsonFormat5(PodCost.apply) @@ -86,7 +93,35 @@ trait KubernetesJsonSupport extends SprayJsonSupport with DefaultJsonProtocol { implicit val podMetadataFormat: JsonFormat[PodMetadata] = jsonFormat1(PodMetadata.apply) implicit val podFormat: RootJsonFormat[Pod] = jsonFormat1(Pod.apply) - implicit val replicaAnnotationFormat: JsonFormat[ReplicaAnnotation] = jsonFormat1(ReplicaAnnotation.apply) + implicit val replicaAnnotationFormat: RootJsonFormat[ReplicaAnnotation] = + new RootJsonFormat[ReplicaAnnotation] { + // Not sure if we ever write this out, but if we do, this will let us write out exactly what we took in + def write(ra: ReplicaAnnotation): JsValue = + if (ra.revision.nonEmpty && ra.source.nonEmpty) { + JsObject(ra.otherAnnotations + (ra.source -> JsString(ra.revision))) + } else JsObject(ra.otherAnnotations) + + def read(json: JsValue): ReplicaAnnotation = { + json match { + case JsObject(fields) => + _revisionAnnotations.revisionAnnotations.find { annotation => + fields.get(annotation).exists(_.isInstanceOf[JsString]) + } match { + case Some(winningAnnotation) => + ReplicaAnnotation( + fields(winningAnnotation).asInstanceOf[JsString].value, + winningAnnotation, + fields - winningAnnotation) + + case None => + ReplicaAnnotation("", "", fields) + } + + case _ => spray.json.deserializationError("expected an object") + } + } + } + implicit val replicaSetMedatataFormat: JsonFormat[ReplicaSetMetadata] = jsonFormat1(ReplicaSetMetadata.apply) implicit val podReplicaSetFormat: RootJsonFormat[ReplicaSet] = jsonFormat1(ReplicaSet.apply) } diff --git a/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesSettings.scala b/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesSettings.scala index f00fdfb2c..706fdf0f2 100644 --- a/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesSettings.scala +++ b/rolling-update-kubernetes/src/main/scala/akka/rollingupdate/kubernetes/KubernetesSettings.scala @@ -4,11 +4,13 @@ package akka.rollingupdate.kubernetes -import scala.concurrent.duration._ import akka.util.JavaDurationConverters._ import akka.annotation.InternalApi import com.typesafe.config.Config +import scala.concurrent.duration._ +import scala.jdk.CollectionConverters.ListHasAsScala + /** * INTERNAL API */ @@ -47,7 +49,8 @@ private[kubernetes] object KubernetesSettings { config.getString("pod-name"), config.getBoolean("secure-api-server"), config.getDuration("api-service-request-timeout").asScala, - customResourceSettings + customResourceSettings, + config.getStringList("revision-annotation").asScala.toList ) } } @@ -67,8 +70,17 @@ private[kubernetes] class KubernetesSettings( val secure: Boolean, val apiServiceRequestTimeout: FiniteDuration, val customResourceSettings: CustomResourceSettings, + val revisionAnnotations: Seq[String], val bodyReadTimeout: FiniteDuration = 1.second -) +) extends HasRevisionAnnotations + +/** + * INTERNAL API + */ +@InternalApi +private[kubernetes] trait HasRevisionAnnotations { + def revisionAnnotations: Seq[String] +} /** * INTERNAL API