From 6c3f7f50df3cbf5364eedd20fbffac17462d7d80 Mon Sep 17 00:00:00 2001 From: Raymond Roestenburg Date: Fri, 23 Jul 2021 15:09:58 +0200 Subject: [PATCH] Fix for VolumeMount API #1072 (#1073) * Added support back in for --volume-mount. Added test. Some refactoring, added check for missing volume mounts. Better error message. Cleanup. Fix for compile. Added a comment. * Added the Cloudflow CRD. (#1074) * Cloudflow Custom Resource Definition (CRD). * Comment to remove endpoint statuses from CRD status, not used. * Added docs to CRD fields. * Remove tmp file. --- cloudflow-crd.yaml | 75 ----- .../cloudflow/execution/DeployExecution.scala | 11 +- .../execution/WithConfiguration.scala | 2 +- .../execution/WithUpdateVolumeMounts.scala | 116 +++++++ .../kubeclient/KubeClientFabric8.scala | 1 + .../WithUpdateVolumeMountsSpec.scala | 177 ++++++++++ .../kubernetes/cloudflow-crd.yaml | 317 ++++++++++++++++++ .../src/main/scala/akka/datap/crd/App.scala | 5 +- 8 files changed, 623 insertions(+), 81 deletions(-) delete mode 100644 cloudflow-crd.yaml create mode 100644 core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/WithUpdateVolumeMounts.scala create mode 100644 core/cloudflow-cli/src/test/scala/akka/cli/cloudflow/execution/WithUpdateVolumeMountsSpec.scala create mode 100644 core/cloudflow-crd/kubernetes/cloudflow-crd.yaml diff --git a/cloudflow-crd.yaml b/cloudflow-crd.yaml deleted file mode 100644 index 7a7d16308..000000000 --- a/cloudflow-crd.yaml +++ /dev/null @@ -1,75 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1beta1 -kind: CustomResourceDefinition -metadata: - creationTimestamp: "2021-05-21T11:25:46Z" - generation: 1 - managedFields: - - apiVersion: apiextensions.k8s.io/v1 - fieldsType: FieldsV1 - fieldsV1: - f:status: - f:acceptedNames: - f:kind: {} - f:listKind: {} - f:plural: {} - f:shortNames: {} - f:singular: {} - f:conditions: {} - manager: kube-apiserver - operation: Update - - apiVersion: apiextensions.k8s.io/v1beta1 - fieldsType: FieldsV1 - fieldsV1: - f:spec: - f:conversion: - .: {} - f:strategy: {} - f:group: {} - f:names: - f:kind: {} - f:listKind: {} - f:plural: {} - f:shortNames: {} - f:singular: {} - f:preserveUnknownFields: {} - f:scope: {} - f:subresources: - .: {} - f:status: {} - f:version: {} - f:versions: {} - f:status: - f:storedVersions: {} - manager: okhttp - operation: Update - name: cloudflowapplications.cloudflow.lightbend.com -spec: - conversion: - strategy: None - group: cloudflow.lightbend.com - names: - kind: CloudflowApplication - listKind: CloudflowApplicationList - plural: cloudflowapplications - shortNames: - - cloudflowapp - singular: cloudflowapplication - preserveUnknownFields: true - scope: Namespaced - subresources: - status: {} - version: v1alpha1 - versions: - - name: v1alpha1 - served: true - storage: true -status: - acceptedNames: - kind: CloudflowApplication - listKind: CloudflowApplicationList - plural: cloudflowapplications - shortNames: - - cloudflowapp - singular: cloudflowapplication - storedVersions: - - v1alpha1 diff --git a/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/DeployExecution.scala b/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/DeployExecution.scala index f7836ae9c..6e1cad0ac 100644 --- a/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/DeployExecution.scala +++ b/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/DeployExecution.scala @@ -24,6 +24,7 @@ final case class DeployExecution(d: Deploy, client: KubeClient, logger: CliLogge extends Execution[DeployResult] with WithProtocolVersion with WithUpdateReplicas + with WithUpdateVolumeMounts with WithConfiguration { import DeployExecution._ @@ -105,7 +106,7 @@ final case class DeployExecution(d: Deploy, client: KubeClient, logger: CliLogge .flatten if (!res.isEmpty) { - val ex: Throwable = res.map(_._2).flatten.headOption.getOrElse(null) + val ex: Throwable = res.flatMap(_._2).headOption.getOrElse(null) Failure(CliException(res.map(_._1).mkString("\n"), ex)) } else { Success(()) @@ -113,7 +114,7 @@ final case class DeployExecution(d: Deploy, client: KubeClient, logger: CliLogge } private def referencedKafkaSecretExists(appCr: App.Cr, kafkaClusters: () => Try[List[String]]): Try[Unit] = { - val expectedClusters = appCr.spec.deployments.map(_.portMappings.values.map(_.cluster)).flatten.flatten.distinct + val expectedClusters = appCr.spec.deployments.flatMap(_.portMappings.values.map(_.cluster)).flatten.distinct if (expectedClusters.nonEmpty) { (for { @@ -160,7 +161,11 @@ final case class DeployExecution(d: Deploy, client: KubeClient, logger: CliLogge currentAppCr <- client.readCloudflowApp(localApplicationCr.spec.appId) clusterReplicas = getStreamletsReplicas(currentAppCr) clusterApplicationCr <- updateReplicas(localApplicationCr, clusterReplicas) - applicationCr <- updateReplicas(clusterApplicationCr, d.scales) + applicationCrReplicas <- updateReplicas(clusterApplicationCr, d.scales) + applicationCr <- updateVolumeMounts( + applicationCrReplicas, + d.volumeMounts, + () => client.getPvcs(namespace = applicationCrReplicas.spec.appId)) image <- getImageReference(applicationCr) diff --git a/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/WithConfiguration.scala b/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/WithConfiguration.scala index 61085ab49..45e026572 100644 --- a/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/WithConfiguration.scala +++ b/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/WithConfiguration.scala @@ -99,7 +99,7 @@ trait WithConfiguration { private def validateTopicIds(crApp: App.Cr, cloudflowConfig: CloudflowConfig.CloudflowRoot): Try[Unit] = { val configTopics = cloudflowConfig.cloudflow.topics.keys.toSeq.distinct - val crTopics = crApp.spec.deployments.map(_.portMappings.values.map(_.id)).flatten.distinct + val crTopics = crApp.spec.deployments.flatMap(_.portMappings.values.map(_.id)).distinct configTopics.diff(crTopics) match { case Nil => Success(()) diff --git a/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/WithUpdateVolumeMounts.scala b/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/WithUpdateVolumeMounts.scala new file mode 100644 index 000000000..b928ac913 --- /dev/null +++ b/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/execution/WithUpdateVolumeMounts.scala @@ -0,0 +1,116 @@ +/* + * Copyright (C) 2021 Lightbend Inc. + */ + +package akka.cli.cloudflow.execution + +import akka.cli.cloudflow.CliException +import akka.datap.crd.App + +import scala.util.{ Failure, Success, Try } + +// This can be deprecated when VolumeMount API is deprecated. +trait WithUpdateVolumeMounts { + def updateVolumeMounts( + crApp: App.Cr, + volumeMountsArgs: Map[String, String], + pvcs: () => Try[List[String]]): Try[App.Cr] = { + for { + streamletVolumeNameToPvc <- streamletVolumeNameToPvcMap(crApp, volumeMountsArgs, pvcs) + _ <- missingStreamletVolumeMountNames(crApp, streamletVolumeNameToPvc.keys) + } yield crApp.copy(spec = crApp.spec.copy( + deployments = updatedDeployments(crApp, streamletVolumeNameToPvc), + streamlets = updatedStreamlets(crApp, streamletVolumeNameToPvc))) + } + + private def streamletVolumeNameToPvcMap( + crApp: App.Cr, + volumeMountsArgs: Map[String, String], + pvcs: () => Try[List[String]]): Try[Map[(String, String), String]] = { + for { + existingPvcs <- pvcs() + map <- Try { + volumeMountsArgs.map { + case (streamletVolumeNamePath, pvcName) => + if (!existingPvcs.contains(pvcName)) { + throw new CliException( + s"Cannot find persistent volume claim '$pvcName' specified via --volume-mount argument.") + } + // volumeMounts Map is "." -> pvc-name + val parts = streamletVolumeNamePath.split("\\.").toList + if (parts.size != 2) { + throw new CliException( + "--volume-mount argument is invalid, please provide as --volume-mount .=") + } + val streamletName = parts(0) + val volumeMountName = parts(1) + if (crApp.spec.deployments.find(_.streamletName == streamletName).isEmpty) { + throw new CliException(s"Cannot find streamlet '$streamletName' in --volume-mount argument") + } + + if (crApp.spec.deployments + .filter(_.streamletName == streamletName) + .flatMap(_.volumeMounts) + .find(_.name == volumeMountName) + .isEmpty) { + throw new CliException( + s"Cannot find volume mount name '$volumeMountName' for streamlet '$streamletName' in --volume-mount argument") + } + (streamletName, volumeMountName) -> pvcName + }.toMap + } + } yield map + } + + private def missingStreamletVolumeMountNames( + crApp: App.Cr, + streamletVolumeNamesFromArgs: Iterable[(String, String)]): Try[Unit] = { + Try { + val missing = (streamletVolumeMountNamesInCr(crApp) -- streamletVolumeNamesFromArgs.toSet) + .map { case (streamletName, volumeName) => s"$streamletName.$volumeName" } + .toSeq + .sorted + if (missing.nonEmpty) { + def plural = s"""${if (missing.size > 1) "s" else ""}""" + throw new CliException( + s"""Please provide persistent volume name$plural with --volume-mount argument$plural (replace 'pvc-name'$plural with correct value$plural):\n + |${missing.map(m => s"--volume-mount $m=pvc-name").mkString("\n")} + """.stripMargin) + } + () + } + } + + private def streamletVolumeMountNamesInCr(crApp: App.Cr): Set[(String, String)] = { + crApp.spec.deployments + .flatMap(deployment => deployment.volumeMounts.map(vm => deployment.streamletName -> vm.name)) + .toSet + } + + private def updatedDeployments( + crApp: App.Cr, + streamletVolumeNameToPvc: Map[(String, String), String]): Seq[App.Deployment] = { + crApp.spec.deployments.map { deployment => + deployment.copy(volumeMounts = deployment.volumeMounts.map { vmd => + streamletVolumeNameToPvc + .get((deployment.streamletName, vmd.name)) + .map(pvcName => vmd.copy(pvcName = Some(pvcName))) + .getOrElse(vmd) + }) + } + } + + private def updatedStreamlets( + crApp: App.Cr, + streamletVolumeNameToPvc: Map[(String, String), String]): Seq[App.Streamlet] = { + crApp.spec.streamlets.map { streamlet => + streamlet.copy(descriptor = streamlet.descriptor.copy(volumeMounts = streamlet.descriptor.volumeMounts.map { + vmd => + streamletVolumeNameToPvc + .get((streamlet.name, vmd.name)) + .map(pvcName => vmd.copy(pvcName = Some(pvcName))) + .getOrElse(vmd) + })) + } + } +} diff --git a/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/kubeclient/KubeClientFabric8.scala b/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/kubeclient/KubeClientFabric8.scala index 04cdf1f81..3822222be 100644 --- a/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/kubeclient/KubeClientFabric8.scala +++ b/core/cloudflow-cli/src/main/scala/akka/cli/cloudflow/kubeclient/KubeClientFabric8.scala @@ -187,6 +187,7 @@ class KubeClientFabric8( val res = models.ApplicationStatus( summary = getCRSummary(app), status = appStatus, + // FIXME, remove in a breaking CRD change, the endpoint statuses are not updated anymore. endpointsStatuses = Try(app.status.endpointStatuses).toOption .filterNot(_ == null) .map(_.map(getEndpointStatus)) diff --git a/core/cloudflow-cli/src/test/scala/akka/cli/cloudflow/execution/WithUpdateVolumeMountsSpec.scala b/core/cloudflow-cli/src/test/scala/akka/cli/cloudflow/execution/WithUpdateVolumeMountsSpec.scala new file mode 100644 index 000000000..07d9797b0 --- /dev/null +++ b/core/cloudflow-cli/src/test/scala/akka/cli/cloudflow/execution/WithUpdateVolumeMountsSpec.scala @@ -0,0 +1,177 @@ +/* + * Copyright (C) 2021 Lightbend Inc. + */ + +package akka.cli.cloudflow.execution + +import scala.util.Try +import akka.cli.cloudflow.CliLogger +import akka.cloudflow.config.CloudflowConfig +import akka.datap.crd.App +import com.typesafe.config.ConfigFactory +import io.fabric8.kubernetes.client.utils.Serialization +import org.scalatest.TryValues +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers + +class WithUpdateVolumesMountsSpec extends AnyFlatSpec with WithUpdateVolumeMounts with Matchers with TryValues { + val emptyConfig = Serialization.jsonMapper().readTree("{}") + val streamletName = "my-streamlet" + def crWithVolumeMounts(volumeMounts: Seq[App.VolumeMountDescriptor]) = { + App.Cr( + metadata = null, + spec = App.Spec( + appId = "", + appVersion = "", + deployments = Seq( + App.Deployment( + name = "some-app-id", + runtime = "akka", + image = "docker-registry.foo.com/lightbend/call-record-pipeline:277-ceb9629", + streamletName = streamletName, + className = "cloudflow.operator.runner.AkkaRunner", + secretName = streamletName, + config = emptyConfig, + portMappings = Map("valid" -> App + .PortMapping(id = "valid-metrics", config = null, cluster = Some("deployment-named-cluster"))), + volumeMounts = volumeMounts, + endpoint = None, + replicas = None)), + agentPaths = Map(), + version = None, + libraryVersion = None, + streamlets = Seq( + App.Streamlet( + name = streamletName, + descriptor = App.Descriptor( + attributes = Seq(), + className = "", + volumeMounts = volumeMounts, + inlets = Seq(), + labels = Seq(), + outlets = Seq(), + runtime = "", + description = "", + configParameters = Seq()))))) + } + + "--volume-mount" should "not update the CR when not specified" in { + // Arrange + val appCr = crWithVolumeMounts(Seq()) + + // Act + val updatedCr = updateVolumeMounts(appCr, Map(), () => Try(List("pvc-name"))) + + // Assert + updatedCr.success.value shouldBe appCr + } + + it should "update CR deployments and descriptors if specified correctly and pvcs can be found" in { + val volumeName = "volume" + // Arrange + val volumeMountFromApi = App.VolumeMountDescriptor(volumeName, "/mnt/data", "ReadWriteMany", Some("")) + val expectedVolumeMount = App.VolumeMountDescriptor(volumeName, "/mnt/data", "ReadWriteMany", Some("my-pvc")) + val appCr = crWithVolumeMounts(Seq(volumeMountFromApi)) + + // Act + val updatedCr = + updateVolumeMounts( + appCr, + Map(s"$streamletName.$volumeName" -> "my-pvc"), + () => Try(List("my-pvc", "my-other-pvc"))) + + // Assert + val volumeMountsInDescriptor = + updatedCr.success.value.spec.streamlets + .find(_.name == streamletName) + .map(_.descriptor.volumeMounts) + .getOrElse(Seq()) + volumeMountsInDescriptor should contain(expectedVolumeMount) + val volumeMountsInDeployment = + updatedCr.success.value.spec.deployments + .find(_.streamletName == streamletName) + .map(_.volumeMounts) + .getOrElse(Seq()) + volumeMountsInDeployment should contain(expectedVolumeMount) + } + + it should "fail if pvc cannot be found" in { + val volumeName = "volume" + // Arrange + val volumeMountFromApi = App.VolumeMountDescriptor(volumeName, "/mnt/data", "ReadWriteMany", Some("")) + val appCr = crWithVolumeMounts(Seq(volumeMountFromApi)) + + // Act + val updatedCr = + updateVolumeMounts( + appCr, + Map(s"$streamletName.$volumeName" -> "my-pvc-does-not-exist"), + () => Try(List("my-pvc", "my-other-pvc"))) + + // Assert + updatedCr.isSuccess shouldBe false + } + + it should "fail if the streamlet cannot be found" in { + val volumeName = "volume" + // Arrange + val volumeMountFromApi = App.VolumeMountDescriptor(volumeName, "/mnt/data", "ReadWriteMany", Some("")) + val appCr = crWithVolumeMounts(Seq(volumeMountFromApi)) + + // Act + val updatedCr = + updateVolumeMounts(appCr, Map(s"non-existing.$volumeName" -> "my-pvc"), () => Try(List("my-pvc", "my-other-pvc"))) + + // Assert + updatedCr.isSuccess shouldBe false + } + + it should "fail if the volume cannot be found" in { + val volumeName = "volume" + // Arrange + val volumeMountFromApi = App.VolumeMountDescriptor(volumeName, "/mnt/data", "ReadWriteMany", Some("")) + val appCr = crWithVolumeMounts(Seq(volumeMountFromApi)) + + // Act + val updatedCr = + updateVolumeMounts( + appCr, + Map(s"$streamletName.notexisting-volume-name" -> "my-pvc"), + () => Try(List("my-pvc", "my-other-pvc"))) + + // Assert + updatedCr.isSuccess shouldBe false + } + + it should "fail if the --volume-mount does not follow ." in { + val volumeName = "volume" + // Arrange + val volumeMountFromApi = App.VolumeMountDescriptor(volumeName, "/mnt/data", "ReadWriteMany", Some("")) + val appCr = crWithVolumeMounts(Seq(volumeMountFromApi)) + + // Act + val updatedCr = + updateVolumeMounts( + appCr, + Map(s"$streamletName$volumeName" -> "my-pvc"), + () => Try(List("my-pvc", "my-other-pvc"))) + + // Assert + updatedCr.isSuccess shouldBe false + } + + it should "fail if the --volume-mount does not specify the volume mount that the streamlet needs" in { + val volumeName = "volume" + // Arrange + val volumeMountFromApi = App.VolumeMountDescriptor(volumeName, "/mnt/data", "ReadWriteMany", Some("")) + val appCr = crWithVolumeMounts(Seq(volumeMountFromApi)) + + // Act + val updatedCr = + updateVolumeMounts(appCr, Map(), () => Try(List("my-pvc", "my-other-pvc"))) + + // Assert + updatedCr.isSuccess shouldBe false + } + +} diff --git a/core/cloudflow-crd/kubernetes/cloudflow-crd.yaml b/core/cloudflow-crd/kubernetes/cloudflow-crd.yaml new file mode 100644 index 000000000..8b4ca0b3a --- /dev/null +++ b/core/cloudflow-crd/kubernetes/cloudflow-crd.yaml @@ -0,0 +1,317 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: cloudflowapplications.cloudflow.lightbend.com +spec: + group: cloudflow.lightbend.com + names: + kind: CloudflowApplication + listKind: CloudflowApplicationList + plural: cloudflowapplications + shortNames: + - cloudflowapp + singular: cloudflowapplication + scope: Namespaced + versions: + - name: v1alpha1 + served: true + storage: true + schema: + openAPIV3Schema: + type: object + properties: + spec: + type: object + properties: + app_id: + type: string + description: 'The ID of the application.' + app_version: + type: string + description: 'The version of the application.' + version: + type: string + description: 'The version of the application descriptor.' + library_version: + type: string + description: 'The version of the Cloudflow library used.' + agent_paths: + type: object + description: 'Not used anymore, kept for compatibility.' + additionalProperties: + type: string + deployments: + type: array + description: 'The streamlet deployments in this application.' + items: + type: object + properties: + class_name: + type: string + description: 'The Streamlet class.' + config: + type: object + description: 'Configuration for the streamlet.' + image: + type: string + description: 'The image that should be used to deploy the streamlet.' + name: + type: string + description: 'The name of the streamlet inside the application, formatted as .' + port_mappings: + type: object + additionalProperties: + type: object + properties: + id: + type: string + description: 'The ID of the topic' + config: + type: object + description: 'The topic configuration' + cluster: + type: string + description: 'Optional, the named cluster that should be used to connect to the topic' + required: + - id + - config + volume_mounts: + type: array + items: + type: object + properties: + name: + type: string + description: 'The name of the volume.' + path: + type: string + description: 'The path to mount.' + access_mode: + type: string + description: 'The access mode, ReadWriteMany or ReadOnlyMany' + pvc_name: + type: string + description: 'The name of the persistent volume claim.' + required: + - name + - path + - access_mode + # to keep compatibility with existing format. + nullable: true + runtime: + type: string + description: 'The runtime that should be used for the streamlet.' + streamlet_name: + type: string + description: 'The name of the streamlet.' + secret_name: + type: string + description: 'The name of the secret that contains configuration for the streamlet.' + endpoint: + type: object + properties: + app_id: + type: string + description: 'The ID of the app.' + streamlet: + type: string + description: 'The streamlet name.' + container_port: + type: integer + description: 'The port that the container exposes through a service.' + replicas: + description: 'The number of streamlet replicas to deploy.' + type: integer + required: + - class_name + - image + - name + - runtime + default: [] + streamlets: + type: array + description: 'The types of streamlets used in this application.' + items: + type: object + properties: + name: + type: string + descriptor: + type: object + properties: + attributes: + type: array + description: 'Internal attributes that are used to enable runtime-features.' + items: + type: object + properties: + attribute_name: + type: string + config_path: + type: string + class_name: + type: string + description: 'The streamlet class.' + config_parameters: + type: array + items: + type: object + properties: + key: + type: string + description: 'The config parameter key.' + description: + type: string + description: 'The description of the config parameter.' + validation_type: + type: string + description: 'The type of validation that must take place in the CLI.' + validation_pattern: + type: string + description: 'The validation regex pattern.' + default_value: + type: string + description: 'The default value for the config parameter.' + volume_mounts: + type: array + items: + type: object + properties: + name: + type: string + description: 'The name of the streamlet volume.' + path: + type: string + description: 'The mount path.' + access_mode: + type: string + description: 'The access mode, ReadWriteMany or ReadOnlyMany' + pvc_name: + type: string + description: 'The name of the persistent volume claim.' + required: + - name + - path + - access_mode + inlets: + type: array + items: + type: object + properties: + name: + type: string + schema: + type: object + properties: + fingerprint: + type: string + description: 'The schema fingerprint.' + schema: + type: string + description: 'The full schema, field is kept for backwards compatibility, is no longer provided.' + name: + type: string + description: 'The name of the schema.' + format: + type: string + description: 'The schema format, for instance avro or proto.' + outlets: + type: array + items: + type: object + properties: + name: + type: string + schema: + type: object + properties: + fingerprint: + type: string + description: 'The schema fingerprint.' + schema: + type: string + description: 'The full schema, field is kept for backwards compatibility, is no longer provided.' + name: + type: string + description: 'The name of the schema.' + format: + type: string + description: 'The schema format, for instance avro or proto.' + labels: + type: array + items: + type: string + description: 'labels describing the type of streamlet, not used anymore, kept for backwards compatibility.' + runtime: + type: string + description: 'The runtime that this streamlet type can run on.' + description: + type: string + description: 'A description of the streamlet, not used anymore, kept for backwards compatibility.' + required: + - class_name + - runtime + required: + - app_id + - deployments + - streamlets + x-kubernetes-preserve-unknown-fields: true + status: + type: object + description: 'The status of the application.' + properties: + app_id: + type: string + app_version: + type: string + app_status: + type: string + app_message: + type: string + # endpoint statuses are not updated, kept for backwards compatibility. + endpoint_statuses: + type: array + items: + type: object + properties: + streamlet_name: + type: string + url: + type: string + streamlet_statuses: + type: array + items: + type: object + properties: + streamlet_name: + type: string + expected_pod_count: + type: integer + pod_statuses: + type: array + items: + type: object + properties: + name: + type: string + ready: + type: string + nr_of_containers_ready: + type: integer + restarts: + type: integer + status: + type: string + x-kubernetes-preserve-unknown-fields: true + subresources: + status: {} +status: + acceptedNames: + kind: CloudflowApplication + listKind: CloudflowApplicationList + plural: cloudflowapplications + shortNames: + - cloudflowapp + singular: cloudflowapplication + storedVersions: + - v1alpha1 diff --git a/core/cloudflow-crd/src/main/scala/akka/datap/crd/App.scala b/core/cloudflow-crd/src/main/scala/akka/datap/crd/App.scala index 1de9b27c0..26c3b0351 100644 --- a/core/cloudflow-crd/src/main/scala/akka/datap/crd/App.scala +++ b/core/cloudflow-crd/src/main/scala/akka/datap/crd/App.scala @@ -230,9 +230,9 @@ object App { @JsonProperty("name") name: String, @JsonProperty("port_mappings") - portMappings: Map[String, PortMapping], + portMappings: Map[String, PortMapping] = Map(), @JsonProperty("volume_mounts") - volumeMounts: immutable.Seq[VolumeMountDescriptor], + volumeMounts: immutable.Seq[VolumeMountDescriptor] = immutable.Seq(), @JsonProperty("runtime") runtime: String, @JsonProperty("streamlet_name") @@ -314,6 +314,7 @@ object App { appStatus: String, @JsonProperty("app_message") appMessage: String, + // FIXME not updated anymore, remove in a next CRD version. @JsonProperty("endpoint_statuses") endpointStatuses: immutable.Seq[EndpointStatus], @JsonProperty("streamlet_statuses")