From 5e34850f4b3cc9c80fda4f0df245afcaa29b1daf Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Mon, 19 Oct 2020 10:45:19 +0300 Subject: [PATCH] refactor(plus): Remove the `spec` nesting level on both input and output (#347) Removed the need to specify various `spec` properties for both pre and post instantiation. Applies to all constructs that used to accept a pod spec. ### Before ```ts const deployment = new kplus.Deployment(this, text, { spec: { podSpecTemplate: { containers: [ new kplus.Container({ image: 'hashicorp/http-echo', args: [ '-text', text ] }) ] } } }); deployment.spec.podSpecTemplate.addContainer(...) ``` ### After ```ts const deployment = new kplus.Deployment(this, text, { containers: [ new kplus.Container({ image: 'hashicorp/http-echo', args: [ '-text', text ] }) ] }); deployment.addContainer(...) ``` BREAKING CHANGE: `spec` was removed from all cdk8s+ constructs and that now have a flat structure. See [Example](https://github.com/awslabs/cdk8s/tree/master/packages/cdk8s-plus#at-a-glance) for new usage. * **plus**: Construct id's for deployment will change due to a latent bug that appended the word `pod` to them. *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../__snapshots__/main.test.ts.snap | 2 +- .../cdk8s-plus-elasticsearch-query/main.ts | 8 +- .../typescript/cdk8s-plus-ingress/main.ts | 16 +- packages/cdk8s-plus/API.md | 525 +++++++++++------- packages/cdk8s-plus/README.md | 90 ++- packages/cdk8s-plus/src/config-map.ts | 5 +- packages/cdk8s-plus/src/deployment.ts | 168 +++--- packages/cdk8s-plus/src/index.ts | 2 +- packages/cdk8s-plus/src/ingress.ts | 34 +- packages/cdk8s-plus/src/job.ts | 124 ++--- packages/cdk8s-plus/src/pod.ts | 314 +++++++---- packages/cdk8s-plus/src/secret.ts | 5 +- packages/cdk8s-plus/src/service-account.ts | 5 +- packages/cdk8s-plus/src/service.ts | 307 +++++----- packages/cdk8s-plus/test/deployment.test.ts | 230 +++----- packages/cdk8s-plus/test/ingress.test.ts | 80 +-- packages/cdk8s-plus/test/job.test.ts | 128 ++--- packages/cdk8s-plus/test/pod.test.ts | 212 ++----- packages/cdk8s-plus/test/service.test.ts | 250 ++++----- 19 files changed, 1200 insertions(+), 1305 deletions(-) diff --git a/examples/typescript/cdk8s-plus-elasticsearch-query/__snapshots__/main.test.ts.snap b/examples/typescript/cdk8s-plus-elasticsearch-query/__snapshots__/main.test.ts.snap index 7083921d9f..c6ea7c9124 100644 --- a/examples/typescript/cdk8s-plus-elasticsearch-query/__snapshots__/main.test.ts.snap +++ b/examples/typescript/cdk8s-plus-elasticsearch-query/__snapshots__/main.test.ts.snap @@ -122,7 +122,7 @@ function doSearch(uri, callback) { "apiVersion": "apps/v1", "kind": "Deployment", "metadata": Object { - "name": "test-chart-deployment-pod-1ef542cf", + "name": "test-chart-deployment-c5d38cbe", }, "spec": Object { "replicas": 1, diff --git a/examples/typescript/cdk8s-plus-elasticsearch-query/main.ts b/examples/typescript/cdk8s-plus-elasticsearch-query/main.ts index 23b94eb35e..c8e6b693a6 100644 --- a/examples/typescript/cdk8s-plus-elasticsearch-query/main.ts +++ b/examples/typescript/cdk8s-plus-elasticsearch-query/main.ts @@ -64,12 +64,8 @@ export class MyChart extends Chart { container.mount(workingDir, volume); const deployment = new kplus.Deployment(this, 'Deployment', { - spec: { - replicas: 1, - podSpecTemplate: { - containers: [container] - } - } + replicas: 1, + containers: [container] }) deployment.expose(9000); diff --git a/examples/typescript/cdk8s-plus-ingress/main.ts b/examples/typescript/cdk8s-plus-ingress/main.ts index 3ac3b81864..5df9f94c1d 100644 --- a/examples/typescript/cdk8s-plus-ingress/main.ts +++ b/examples/typescript/cdk8s-plus-ingress/main.ts @@ -16,16 +16,12 @@ export class MyChart extends Chart { private echoBackend(text: string) { const deploy = new kplus.Deployment(this, text, { - spec: { - podSpecTemplate: { - containers: [ - new kplus.Container({ - image: 'hashicorp/http-echo', - args: [ '-text', text ] - }) - ] - } - } + containers: [ + new kplus.Container({ + image: 'hashicorp/http-echo', + args: [ '-text', text ] + }) + ] }); return kplus.IngressBackend.fromService(deploy.expose(5678)); diff --git a/packages/cdk8s-plus/API.md b/packages/cdk8s-plus/API.md index 6c390f15ad..d94343cde4 100644 --- a/packages/cdk8s-plus/API.md +++ b/packages/cdk8s-plus/API.md @@ -7,20 +7,18 @@ Name|Description [ConfigMap](#cdk8s-plus-configmap)|ConfigMap holds configuration data for pods to consume. [Container](#cdk8s-plus-container)|A single application container that you want to run within a pod. [Deployment](#cdk8s-plus-deployment)|A Deployment provides declarative updates for Pods and ReplicaSets. -[DeploymentSpecDefinition](#cdk8s-plus-deploymentspecdefinition)|DeploymentSpec is the specification of the desired behavior of the Deployment. [Duration](#cdk8s-plus-duration)|Represents a length of time. [EnvValue](#cdk8s-plus-envvalue)|Utility class for creating reading env values from various sources. [Ingress](#cdk8s-plus-ingress)|Ingress is a collection of rules that allow inbound connections to reach the endpoints defined by a backend. [IngressBackend](#cdk8s-plus-ingressbackend)|The backend for an ingress path. [Job](#cdk8s-plus-job)|A Job creates one or more Pods and ensures that a specified number of them successfully terminate. -[JobSpecDefinition](#cdk8s-plus-jobspecdefinition)|*No description* [Pod](#cdk8s-plus-pod)|Pod is a collection of containers that can run on a host. -[PodSpecDefinition](#cdk8s-plus-podspecdefinition)|A description of a pod. +[PodSpec](#cdk8s-plus-podspec)|Provides read/write capabilities ontop of a `PodSpecProps`. +[PodTemplate](#cdk8s-plus-podtemplate)|Provides read/write capabilities ontop of a `PodTemplateProps`. [Resource](#cdk8s-plus-resource)|Base class for all Kubernetes objects in stdk8s. [Secret](#cdk8s-plus-secret)|Kubernetes Secrets let you store and manage sensitive information, such as passwords, OAuth tokens, and ssh keys. [Service](#cdk8s-plus-service)|An abstract way to expose an application running on a set of Pods as a network service. [ServiceAccount](#cdk8s-plus-serviceaccount)|A service account provides an identity for processes that run in a Pod. -[ServiceSpecDefinition](#cdk8s-plus-servicespecdefinition)|A description of a service. [Size](#cdk8s-plus-size)|Represents the amount of digital storage. [Volume](#cdk8s-plus-volume)|Volume represents a named volume in a pod that may be accessed by any container in the pod. @@ -34,7 +32,6 @@ Name|Description [ConfigMapVolumeOptions](#cdk8s-plus-configmapvolumeoptions)|Options for the ConfigMap-based volume. [ContainerProps](#cdk8s-plus-containerprops)|Properties for creating a container. [DeploymentProps](#cdk8s-plus-deploymentprops)|Properties for initialization of `Deployment`. -[DeploymentSpec](#cdk8s-plus-deploymentspec)|Properties for initialization of `DeploymentSpec`. [EmptyDirVolumeOptions](#cdk8s-plus-emptydirvolumeoptions)|Options for volumes populated with an empty directory. [EnvValueFromConfigMapOptions](#cdk8s-plus-envvaluefromconfigmapoptions)|Options to specify an envionment variable value from a ConfigMap key. [EnvValueFromProcessOptions](#cdk8s-plus-envvaluefromprocessoptions)|Options to specify an environment variable value from the process environment. @@ -43,11 +40,11 @@ Name|Description [IngressProps](#cdk8s-plus-ingressprops)|Properties for `Ingress`. [IngressRule](#cdk8s-plus-ingressrule)|Represents the rules mapping the paths under a specified host to the related backend services. [JobProps](#cdk8s-plus-jobprops)|Properties for initialization of `Job`. -[JobSpec](#cdk8s-plus-jobspec)|Properties for initialization of `JobSpec`. [MountOptions](#cdk8s-plus-mountoptions)|Options for mounts. [PathMapping](#cdk8s-plus-pathmapping)|Maps a string key to a path within a volume. [PodProps](#cdk8s-plus-podprops)|Properties for initialization of `Pod`. -[PodSpec](#cdk8s-plus-podspec)|Properties for initialization of `PodSpec`. +[PodSpecProps](#cdk8s-plus-podspecprops)|Properties of a `PodSpec`. +[PodTemplateProps](#cdk8s-plus-podtemplateprops)|Properties of a `PodTemplate`. [ResourceProps](#cdk8s-plus-resourceprops)|Initialization properties for resources. [SecretProps](#cdk8s-plus-secretprops)|*No description* [ServiceAccountProps](#cdk8s-plus-serviceaccountprops)|Properties for initialization of `ServiceAccount`. @@ -55,7 +52,6 @@ Name|Description [ServicePort](#cdk8s-plus-serviceport)|Definition of a service port. [ServicePortOptions](#cdk8s-plus-serviceportoptions)|*No description* [ServiceProps](#cdk8s-plus-serviceprops)|Properties for initialization of `Service`. -[ServiceSpec](#cdk8s-plus-servicespec)|Properties for initialization of `ServiceSpec`. [SizeConversionOptions](#cdk8s-plus-sizeconversionoptions)|Options for how to convert time to a different unit. [TimeConversionOptions](#cdk8s-plus-timeconversionoptions)|Options for how to convert time to a different unit. [VolumeMount](#cdk8s-plus-volumemount)|Mount a volume from the pod to the container. @@ -66,6 +62,8 @@ Name|Description Name|Description ----|----------- [IConfigMap](#cdk8s-plus-iconfigmap)|Represents a config map. +[IPodSpec](#cdk8s-plus-ipodspec)|Represents a resource that can be configured with a kuberenets pod spec. (e.g `Deployment`, `Job`, `Pod`, ...). +[IPodTemplate](#cdk8s-plus-ipodtemplate)|Represents a resource that can be configured with a kuberenets pod template. (e.g `Deployment`, `Job`, ...). [IResource](#cdk8s-plus-iresource)|Represents a resource. [ISecret](#cdk8s-plus-isecret)|*No description* [IServiceAccount](#cdk8s-plus-iserviceaccount)|*No description* @@ -310,7 +308,7 @@ The following are typical use cases for Deployments: - Use the status of the Deployment as an indicator that a rollout has stuck. - Clean up older ReplicaSets that you don't need anymore. -__Implements__: [IConstruct](#constructs-iconstruct), [IResource](#cdk8s-plus-iresource) +__Implements__: [IConstruct](#constructs-iconstruct), [IResource](#cdk8s-plus-iresource), [IPodTemplate](#cdk8s-plus-ipodtemplate), [IPodSpec](#cdk8s-plus-ipodspec) __Extends__: [Resource](#cdk8s-plus-resource) ### Initializer @@ -326,8 +324,13 @@ new Deployment(scope: Construct, id: string, props?: DeploymentProps) * **id** (string) *No description* * **props** ([DeploymentProps](#cdk8s-plus-deploymentprops)) *No description* * **metadata** ([ApiObjectMetadata](#cdk8s-apiobjectmetadata)) Metadata that all persisted resources must have, which includes all objects users must create. __*Optional*__ + * **containers** (Array<[Container](#cdk8s-plus-container)>) List of containers belonging to the pod. __*Default*__: No containers. Note that a pod spec must include at least one container. + * **restartPolicy** ([RestartPolicy](#cdk8s-plus-restartpolicy)) Restart policy for all containers within the pod. __*Default*__: RestartPolicy.ALWAYS + * **serviceAccount** ([IServiceAccount](#cdk8s-plus-iserviceaccount)) A service account provides an identity for processes that run in a Pod. __*Default*__: No service account. + * **volumes** (Array<[Volume](#cdk8s-plus-volume)>) List of volumes that can be mounted by containers belonging to the pod. __*Default*__: No volumes. + * **podMetadata** ([ApiObjectMetadata](#cdk8s-apiobjectmetadata)) The pod metadata. __*Optional*__ * **defaultSelector** (boolean) Automatically allocates a pod selector for this deployment. __*Default*__: true - * **spec** ([DeploymentSpec](#cdk8s-plus-deploymentspec)) The spec of the deployment. __*Default*__: An empty spec will be created. + * **replicas** (number) Number of desired pods. __*Default*__: 1 @@ -337,65 +340,61 @@ new Deployment(scope: Construct, id: string, props?: DeploymentProps) Name | Type | Description -----|------|------------- **apiObject**🔹 | [ApiObject](#cdk8s-apiobject) | The underlying cdk8s API object. -**spec**🔹 | [DeploymentSpecDefinition](#cdk8s-plus-deploymentspecdefinition) | Provides access to the underlying spec. +**containers**🔹 | Array<[Container](#cdk8s-plus-container)> | The containers belonging to the pod. +**labelSelector**🔹 | Map | The labels this deployment will match against in order to select pods. +**podMetadata**🔹 | [ApiObjectMetadataDefinition](#cdk8s-apiobjectmetadatadefinition) | Provides read/write access to the underlying pod metadata of the resource. +**replicas**🔹 | number | Number of desired pods. +**volumes**🔹 | Array<[Volume](#cdk8s-plus-volume)> | The volumes associated with this pod. +**restartPolicy**?🔹 | [RestartPolicy](#cdk8s-plus-restartpolicy) | Restart policy for all containers within the pod.
__*Optional*__ +**serviceAccount**?🔹 | [IServiceAccount](#cdk8s-plus-iserviceaccount) | The service account used to run this pod.
__*Optional*__ ### Methods -#### expose(port, options?)🔹 +#### addContainer(container)🔹 -Expose a deployment via a service. - -This is equivalent to running `kubectl expose deployment `. +Add a container to the pod. ```ts -expose(port: number, options?: ExposeOptions): Service +addContainer(container: Container): void ``` -* **port** (number) The port number the service will bind to. -* **options** ([ExposeOptions](#cdk8s-plus-exposeoptions)) Options. - * **serviceType** ([ServiceType](#cdk8s-plus-servicetype)) The type of the exposed service. __*Default*__: ClusterIP. - -__Returns__: -* [Service](#cdk8s-plus-service) - - +* **container** ([Container](#cdk8s-plus-container)) *No description* -## class DeploymentSpecDefinition 🔹 -DeploymentSpec is the specification of the desired behavior of the Deployment. - - -### Initializer +#### addVolume(volume)🔹 +Add a volume to the pod. ```ts -new DeploymentSpecDefinition(props?: DeploymentSpec) +addVolume(volume: Volume): void ``` -* **props** ([DeploymentSpec](#cdk8s-plus-deploymentspec)) *No description* - * **podMetadataTemplate** ([ApiObjectMetadata](#cdk8s-apiobjectmetadata)) Template for pod metadata. __*Optional*__ - * **podSpecTemplate** ([PodSpec](#cdk8s-plus-podspec)) Template for pod specs. __*Optional*__ - * **replicas** (number) Number of desired pods. __*Default*__: 1 +* **volume** ([Volume](#cdk8s-plus-volume)) *No description* -### Properties +#### expose(port, options?)🔹 -Name | Type | Description ------|------|------------- -**labelSelector**🔹 | Map | The labels this deployment will match against in order to select pods. -**podMetadataTemplate**🔹 | [ApiObjectMetadataDefinition](#cdk8s-apiobjectmetadatadefinition) | Template for pod metadata. -**podSpecTemplate**🔹 | [PodSpecDefinition](#cdk8s-plus-podspecdefinition) | Provides access to the underlying pod template spec. -**replicas**?🔹 | number | Number of desired pods.
__*Optional*__ +Expose a deployment via a service. -### Methods +This is equivalent to running `kubectl expose deployment `. + +```ts +expose(port: number, options?: ExposeOptions): Service +``` +* **port** (number) The port number the service will bind to. +* **options** ([ExposeOptions](#cdk8s-plus-exposeoptions)) Options. + * **serviceType** ([ServiceType](#cdk8s-plus-servicetype)) The type of the exposed service. __*Default*__: ClusterIP. + +__Returns__: +* [Service](#cdk8s-plus-service) -#### selectByLabel(key, value)🔹 +#### selectByLabel(key, value)🔹 Configure a label selector to this deployment. @@ -858,7 +857,7 @@ Deleting a Job will clean up the Pods it created. A simple case is to create one The Job object will start a new Pod if the first Pod fails or is deleted (for example due to a node hardware failure or a node reboot). You can also use a Job to run multiple Pods in parallel. -__Implements__: [IConstruct](#constructs-iconstruct), [IResource](#cdk8s-plus-iresource) +__Implements__: [IConstruct](#constructs-iconstruct), [IResource](#cdk8s-plus-iresource), [IPodTemplate](#cdk8s-plus-ipodtemplate), [IPodSpec](#cdk8s-plus-ipodspec) __Extends__: [Resource](#cdk8s-plus-resource) ### Initializer @@ -874,7 +873,12 @@ new Job(scope: Construct, id: string, props?: JobProps) * **id** (string) *No description* * **props** ([JobProps](#cdk8s-plus-jobprops)) *No description* * **metadata** ([ApiObjectMetadata](#cdk8s-apiobjectmetadata)) Metadata that all persisted resources must have, which includes all objects users must create. __*Optional*__ - * **spec** ([JobSpec](#cdk8s-plus-jobspec)) The spec of the job. __*Default*__: An empty spec will be created. + * **containers** (Array<[Container](#cdk8s-plus-container)>) List of containers belonging to the pod. __*Default*__: No containers. Note that a pod spec must include at least one container. + * **restartPolicy** ([RestartPolicy](#cdk8s-plus-restartpolicy)) Restart policy for all containers within the pod. __*Default*__: RestartPolicy.ALWAYS + * **serviceAccount** ([IServiceAccount](#cdk8s-plus-iserviceaccount)) A service account provides an identity for processes that run in a Pod. __*Default*__: No service account. + * **volumes** (Array<[Volume](#cdk8s-plus-volume)>) List of volumes that can be mounted by containers belonging to the pod. __*Default*__: No volumes. + * **podMetadata** ([ApiObjectMetadata](#cdk8s-apiobjectmetadata)) The pod metadata. __*Optional*__ + * **ttlAfterFinished** ([Duration](#cdk8s-plus-duration)) Limits the lifetime of a Job that has finished execution (either Complete or Failed). __*Default*__: If this field is unset, the Job won't be automatically deleted. @@ -884,39 +888,41 @@ new Job(scope: Construct, id: string, props?: JobProps) Name | Type | Description -----|------|------------- **apiObject**🔹 | [ApiObject](#cdk8s-apiobject) | The underlying cdk8s API object. -**spec**🔹 | [JobSpecDefinition](#cdk8s-plus-jobspecdefinition) | +**containers**🔹 | Array<[Container](#cdk8s-plus-container)> | The containers belonging to the pod. +**podMetadata**🔹 | [ApiObjectMetadataDefinition](#cdk8s-apiobjectmetadatadefinition) | Provides read/write access to the underlying pod metadata of the resource. +**volumes**🔹 | Array<[Volume](#cdk8s-plus-volume)> | The volumes associated with this pod. +**restartPolicy**?🔹 | [RestartPolicy](#cdk8s-plus-restartpolicy) | Restart policy for all containers within the pod.
__*Optional*__ +**serviceAccount**?🔹 | [IServiceAccount](#cdk8s-plus-iserviceaccount) | The service account used to run this pod.
__*Optional*__ +**ttlAfterFinished**?🔹 | [Duration](#cdk8s-plus-duration) | TTL before the job is deleted after it is finished.
__*Optional*__ +### Methods -## class JobSpecDefinition 🔹 +#### addContainer(container)🔹 +Add a container to the pod. +```ts +addContainer(container: Container): void +``` + +* **container** ([Container](#cdk8s-plus-container)) *No description* -### Initializer +#### addVolume(volume)🔹 +Add a volume to the pod. ```ts -new JobSpecDefinition(props?: JobSpec) +addVolume(volume: Volume): void ``` -* **props** ([JobSpec](#cdk8s-plus-jobspec)) *No description* - * **podMetadataTemplate** ([ApiObjectMetadata](#cdk8s-apiobjectmetadata)) The metadata of pods created by this job. __*Optional*__ - * **podSpecTemplate** ([PodSpec](#cdk8s-plus-podspec)) The spec of pods created by this job. __*Optional*__ - * **ttlAfterFinished** ([Duration](#cdk8s-plus-duration)) Limits the lifetime of a Job that has finished execution (either Complete or Failed). __*Default*__: If this field is unset, the Job won't be automatically deleted. +* **volume** ([Volume](#cdk8s-plus-volume)) *No description* -### Properties - - -Name | Type | Description ------|------|------------- -**podMetadataTemplate**🔹 | [ApiObjectMetadataDefinition](#cdk8s-apiobjectmetadatadefinition) | The metadata for pods created by this job. -**podSpecTemplate**🔹 | [PodSpecDefinition](#cdk8s-plus-podspecdefinition) | The spec for pods created by this job. -**ttlAfterFinished**?🔹 | [Duration](#cdk8s-plus-duration) | TTL before the job is deleted after it is finished.
__*Optional*__ @@ -927,7 +933,7 @@ Pod is a collection of containers that can run on a host. This resource is created by clients and scheduled onto hosts. -__Implements__: [IConstruct](#constructs-iconstruct), [IResource](#cdk8s-plus-iresource) +__Implements__: [IConstruct](#constructs-iconstruct), [IResource](#cdk8s-plus-iresource), [IPodSpec](#cdk8s-plus-ipodspec) __Extends__: [Resource](#cdk8s-plus-resource) ### Initializer @@ -943,7 +949,10 @@ new Pod(scope: Construct, id: string, props?: PodProps) * **id** (string) *No description* * **props** ([PodProps](#cdk8s-plus-podprops)) *No description* * **metadata** ([ApiObjectMetadata](#cdk8s-apiobjectmetadata)) Metadata that all persisted resources must have, which includes all objects users must create. __*Optional*__ - * **spec** ([PodSpec](#cdk8s-plus-podspec)) The spec of the pod. __*Default*__: An empty spec will be created. + * **containers** (Array<[Container](#cdk8s-plus-container)>) List of containers belonging to the pod. __*Default*__: No containers. Note that a pod spec must include at least one container. + * **restartPolicy** ([RestartPolicy](#cdk8s-plus-restartpolicy)) Restart policy for all containers within the pod. __*Default*__: RestartPolicy.ALWAYS + * **serviceAccount** ([IServiceAccount](#cdk8s-plus-iserviceaccount)) A service account provides an identity for processes that run in a Pod. __*Default*__: No service account. + * **volumes** (Array<[Volume](#cdk8s-plus-volume)>) List of volumes that can be mounted by containers belonging to the pod. __*Default*__: No volumes. @@ -953,14 +962,47 @@ new Pod(scope: Construct, id: string, props?: PodProps) Name | Type | Description -----|------|------------- **apiObject**🔹 | [ApiObject](#cdk8s-apiobject) | The underlying cdk8s API object. -**spec**🔹 | [PodSpecDefinition](#cdk8s-plus-podspecdefinition) | Provides access to the underlying spec. +**containers**🔹 | Array<[Container](#cdk8s-plus-container)> | The containers belonging to the pod. +**volumes**🔹 | Array<[Volume](#cdk8s-plus-volume)> | The volumes associated with this pod. +**restartPolicy**?🔹 | [RestartPolicy](#cdk8s-plus-restartpolicy) | Restart policy for all containers within the pod.
__*Optional*__ +**serviceAccount**?🔹 | [IServiceAccount](#cdk8s-plus-iserviceaccount) | The service account used to run this pod.
__*Optional*__ + +### Methods + + +#### addContainer(container)🔹 + +Add a container to the pod. + +```ts +addContainer(container: Container): void +``` + +* **container** ([Container](#cdk8s-plus-container)) *No description* + + + + +#### addVolume(volume)🔹 + +Add a volume to the pod. + +```ts +addVolume(volume: Volume): void +``` + +* **volume** ([Volume](#cdk8s-plus-volume)) *No description* + -## class PodSpecDefinition 🔹 -A description of a pod. +## class PodSpec 🔹 + +Provides read/write capabilities ontop of a `PodSpecProps`. + +__Implements__: [IPodSpec](#cdk8s-plus-ipodspec) ### Initializer @@ -968,10 +1010,10 @@ A description of a pod. ```ts -new PodSpecDefinition(props?: PodSpec) +new PodSpec(props?: PodSpecProps) ``` -* **props** ([PodSpec](#cdk8s-plus-podspec)) *No description* +* **props** ([PodSpecProps](#cdk8s-plus-podspecprops)) *No description* * **containers** (Array<[Container](#cdk8s-plus-container)>) List of containers belonging to the pod. __*Default*__: No containers. Note that a pod spec must include at least one container. * **restartPolicy** ([RestartPolicy](#cdk8s-plus-restartpolicy)) Restart policy for all containers within the pod. __*Default*__: RestartPolicy.ALWAYS * **serviceAccount** ([IServiceAccount](#cdk8s-plus-iserviceaccount)) A service account provides an identity for processes that run in a Pod. __*Default*__: No service account. @@ -984,39 +1026,73 @@ new PodSpecDefinition(props?: PodSpec) Name | Type | Description -----|------|------------- -**containers**🔹 | Array<[Container](#cdk8s-plus-container)> | List of containers belonging to the pod. -**volumes**🔹 | Array<[Volume](#cdk8s-plus-volume)> | List of volumes that can be mounted by containers belonging to the pod. +**containers**🔹 | Array<[Container](#cdk8s-plus-container)> | The containers belonging to the pod. +**volumes**🔹 | Array<[Volume](#cdk8s-plus-volume)> | The volumes associated with this pod. **restartPolicy**?🔹 | [RestartPolicy](#cdk8s-plus-restartpolicy) | Restart policy for all containers within the pod.
__*Optional*__ **serviceAccount**?🔹 | [IServiceAccount](#cdk8s-plus-iserviceaccount) | The service account used to run this pod.
__*Optional*__ ### Methods -#### addContainer(container)🔹 +#### addContainer(container)🔹 -Adds a container to this pod. +Add a container to the pod. ```ts addContainer(container: Container): void ``` -* **container** ([Container](#cdk8s-plus-container)) The container to add. +* **container** ([Container](#cdk8s-plus-container)) *No description* -#### addVolume(volume)🔹 +#### addVolume(volume)🔹 -Adds a volume to this pod. +Add a volume to the pod. ```ts addVolume(volume: Volume): void ``` -* **volume** ([Volume](#cdk8s-plus-volume)) The volume to add. +* **volume** ([Volume](#cdk8s-plus-volume)) *No description* + + + + + + +## class PodTemplate 🔹 + +Provides read/write capabilities ontop of a `PodTemplateProps`. + +__Implements__: [IPodSpec](#cdk8s-plus-ipodspec), [IPodTemplate](#cdk8s-plus-ipodtemplate), [IPodSpec](#cdk8s-plus-ipodspec) +__Extends__: [PodSpec](#cdk8s-plus-podspec) + +### Initializer + + + + +```ts +new PodTemplate(props?: PodTemplateProps) +``` + +* **props** ([PodTemplateProps](#cdk8s-plus-podtemplateprops)) *No description* + * **containers** (Array<[Container](#cdk8s-plus-container)>) List of containers belonging to the pod. __*Default*__: No containers. Note that a pod spec must include at least one container. + * **restartPolicy** ([RestartPolicy](#cdk8s-plus-restartpolicy)) Restart policy for all containers within the pod. __*Default*__: RestartPolicy.ALWAYS + * **serviceAccount** ([IServiceAccount](#cdk8s-plus-iserviceaccount)) A service account provides an identity for processes that run in a Pod. __*Default*__: No service account. + * **volumes** (Array<[Volume](#cdk8s-plus-volume)>) List of volumes that can be mounted by containers belonging to the pod. __*Default*__: No volumes. + * **podMetadata** ([ApiObjectMetadata](#cdk8s-apiobjectmetadata)) The pod metadata. __*Optional*__ + + +### Properties +Name | Type | Description +-----|------|------------- +**podMetadata**🔹 | [ApiObjectMetadataDefinition](#cdk8s-apiobjectmetadatadefinition) | Provides read/write access to the underlying pod metadata of the resource. @@ -1170,7 +1246,10 @@ new Service(scope: Construct, id: string, props?: ServiceProps) * **id** (string) *No description* * **props** ([ServiceProps](#cdk8s-plus-serviceprops)) *No description* * **metadata** ([ApiObjectMetadata](#cdk8s-apiobjectmetadata)) Metadata that all persisted resources must have, which includes all objects users must create. __*Optional*__ - * **spec** ([ServiceSpec](#cdk8s-plus-servicespec)) The spec of the service. __*Default*__: An empty spec will be created. + * **clusterIP** (string) The IP address of the service and is usually assigned randomly by the master. __*Default*__: Automatically assigned. + * **externalIPs** (Array) A list of IP addresses for which nodes in the cluster will also accept traffic for this service. __*Default*__: No external IPs. + * **ports** (Array<[ServicePort](#cdk8s-plus-serviceport)>) The port exposed by this service. __*Optional*__ + * **type** ([ServiceType](#cdk8s-plus-servicetype)) Determines how the Service is exposed. __*Default*__: ServiceType.ClusterIP @@ -1180,7 +1259,10 @@ new Service(scope: Construct, id: string, props?: ServiceProps) Name | Type | Description -----|------|------------- **apiObject**🔹 | [ApiObject](#cdk8s-apiobject) | The underlying cdk8s API object. -**spec**🔹 | [ServiceSpecDefinition](#cdk8s-plus-servicespecdefinition) | Provides access to the underlying spec. +**ports**🔹 | Array<[ServicePort](#cdk8s-plus-serviceport)> | Ports for this service. +**selector**🔹 | Map | Returns the labels which are used to select pods for this service. +**type**🔹 | [ServiceType](#cdk8s-plus-servicetype) | Determines how the Service is exposed. +**clusterIP**?🔹 | string | The IP address of the service and is usually assigned randomly by the master.
__*Optional*__ ### Methods @@ -1203,6 +1285,40 @@ addDeployment(deployment: Deployment, port: number): void +#### addSelector(label, value)🔹 + +Services defined using this spec will select pods according the provided label. + +```ts +addSelector(label: string, value: string): void +``` + +* **label** (string) The label key. +* **value** (string) The label value. + + + + +#### serve(port, options?)🔹 + +Configure a port the service will bind to. + +This method can be called multiple times. + +```ts +serve(port: number, options?: ServicePortOptions): void +``` + +* **port** (number) The port definition. +* **options** ([ServicePortOptions](#cdk8s-plus-serviceportoptions)) *No description* + * **name** (string) The name of this port within the service. __*Optional*__ + * **nodePort** (number) The port on each node on which this service is exposed when type=NodePort or LoadBalancer. __*Default*__: to auto-allocate a port if the ServiceType of this Service requires one. + * **protocol** ([Protocol](#cdk8s-plus-protocol)) The IP protocol for this port. __*Default*__: Protocol.TCP + * **targetPort** (number) The port number the service will redirect to. __*Default*__: The value of `port` will be used. + + + + ## class ServiceAccount 🔹 @@ -1275,77 +1391,6 @@ __Returns__: -## class ServiceSpecDefinition 🔹 - -A description of a service. - - -### Initializer - - - - -```ts -new ServiceSpecDefinition(props?: ServiceSpec) -``` - -* **props** ([ServiceSpec](#cdk8s-plus-servicespec)) *No description* - * **clusterIP** (string) The IP address of the service and is usually assigned randomly by the master. __*Default*__: Automatically assigned. - * **externalIPs** (Array) A list of IP addresses for which nodes in the cluster will also accept traffic for this service. __*Default*__: No external IPs. - * **ports** (Array<[ServicePort](#cdk8s-plus-serviceport)>) The port exposed by this service. __*Optional*__ - * **type** ([ServiceType](#cdk8s-plus-servicetype)) Determines how the Service is exposed. __*Default*__: ServiceType.ClusterIP - - - -### Properties - - -Name | Type | Description ------|------|------------- -**ports**🔹 | Array<[ServicePort](#cdk8s-plus-serviceport)> | Ports for this service. -**selector**🔹 | Map | Returns the labels which are used to select pods for this service. -**type**🔹 | [ServiceType](#cdk8s-plus-servicetype) | Determines how the Service is exposed. -**clusterIP**?🔹 | string | The IP address of the service and is usually assigned randomly by the master.
__*Optional*__ - -### Methods - - -#### addSelector(label, value)🔹 - -Services defined using this spec will select pods according the provided label. - -```ts -addSelector(label: string, value: string): void -``` - -* **label** (string) The label key. -* **value** (string) The label value. - - - - -#### serve(port, options?)🔹 - -Configure a port the service will bind to. - -This method can be called multiple times. - -```ts -serve(port: number, options?: ServicePortOptions): void -``` - -* **port** (number) The port definition. -* **options** ([ServicePortOptions](#cdk8s-plus-serviceportoptions)) *No description* - * **name** (string) The name of this port within the service. __*Optional*__ - * **nodePort** (number) The port on each node on which this service is exposed when type=NodePort or LoadBalancer. __*Default*__: to auto-allocate a port if the ServiceType of this Service requires one. - * **protocol** ([Protocol](#cdk8s-plus-protocol)) The IP protocol for this port. __*Default*__: Protocol.TCP - * **targetPort** (number) The port number the service will redirect to. __*Default*__: The value of `port` will be used. - - - - - - ## class Size 🔹 Represents the amount of digital storage. @@ -1690,24 +1735,14 @@ Properties for initialization of `Deployment`. Name | Type | Description -----|------|------------- +**containers**?🔹 | Array<[Container](#cdk8s-plus-container)> | List of containers belonging to the pod.
__*Default*__: No containers. Note that a pod spec must include at least one container. **defaultSelector**?🔹 | boolean | Automatically allocates a pod selector for this deployment.
__*Default*__: true **metadata**?🔹 | [ApiObjectMetadata](#cdk8s-apiobjectmetadata) | Metadata that all persisted resources must have, which includes all objects users must create.
__*Optional*__ -**spec**?🔹 | [DeploymentSpec](#cdk8s-plus-deploymentspec) | The spec of the deployment.
__*Default*__: An empty spec will be created. - - - -## struct DeploymentSpec 🔹 - - -Properties for initialization of `DeploymentSpec`. - - - -Name | Type | Description ------|------|------------- -**podMetadataTemplate**?🔹 | [ApiObjectMetadata](#cdk8s-apiobjectmetadata) | Template for pod metadata.
__*Optional*__ -**podSpecTemplate**?🔹 | [PodSpec](#cdk8s-plus-podspec) | Template for pod specs.
__*Optional*__ +**podMetadata**?🔹 | [ApiObjectMetadata](#cdk8s-apiobjectmetadata) | The pod metadata.
__*Optional*__ **replicas**?🔹 | number | Number of desired pods.
__*Default*__: 1 +**restartPolicy**?🔹 | [RestartPolicy](#cdk8s-plus-restartpolicy) | Restart policy for all containers within the pod.
__*Default*__: RestartPolicy.ALWAYS +**serviceAccount**?🔹 | [IServiceAccount](#cdk8s-plus-iserviceaccount) | A service account provides an identity for processes that run in a Pod.
__*Default*__: No service account. +**volumes**?🔹 | Array<[Volume](#cdk8s-plus-volume)> | List of volumes that can be mounted by containers belonging to the pod.
__*Default*__: No volumes. @@ -1793,6 +1828,105 @@ Name | Type | Description +## interface IPodSpec 🔹 + +__Implemented by__: [Deployment](#cdk8s-plus-deployment), [Job](#cdk8s-plus-job), [Pod](#cdk8s-plus-pod), [PodSpec](#cdk8s-plus-podspec), [PodTemplate](#cdk8s-plus-podtemplate) + +Represents a resource that can be configured with a kuberenets pod spec. (e.g `Deployment`, `Job`, `Pod`, ...). + +Use the `PodSpec` class as an implementation helper. + +### Properties + + +Name | Type | Description +-----|------|------------- +**containers**🔹 | Array<[Container](#cdk8s-plus-container)> | The containers belonging to the pod. +**volumes**🔹 | Array<[Volume](#cdk8s-plus-volume)> | The volumes associated with this pod. +**restartPolicy**?🔹 | [RestartPolicy](#cdk8s-plus-restartpolicy) | Restart policy for all containers within the pod.
__*Optional*__ +**serviceAccount**?🔹 | [IServiceAccount](#cdk8s-plus-iserviceaccount) | The service account used to run this pod.
__*Optional*__ + +### Methods + + +#### addContainer(container)🔹 + +Add a container to the pod. + +```ts +addContainer(container: Container): void +``` + +* **container** ([Container](#cdk8s-plus-container)) The container. + + + + +#### addVolume(volume)🔹 + +Add a volume to the pod. + +```ts +addVolume(volume: Volume): void +``` + +* **volume** ([Volume](#cdk8s-plus-volume)) The volume. + + + + + + +## interface IPodTemplate 🔹 + +__Implemented by__: [Deployment](#cdk8s-plus-deployment), [Job](#cdk8s-plus-job), [PodTemplate](#cdk8s-plus-podtemplate) + +Represents a resource that can be configured with a kuberenets pod template. (e.g `Deployment`, `Job`, ...). + +Use the `PodTemplate` class as an implementation helper. + +### Properties + + +Name | Type | Description +-----|------|------------- +**containers**🔹 | Array<[Container](#cdk8s-plus-container)> | The containers belonging to the pod. +**podMetadata**🔹 | [ApiObjectMetadataDefinition](#cdk8s-apiobjectmetadatadefinition) | Provides read/write access to the underlying pod metadata of the resource. +**volumes**🔹 | Array<[Volume](#cdk8s-plus-volume)> | The volumes associated with this pod. +**restartPolicy**?🔹 | [RestartPolicy](#cdk8s-plus-restartpolicy) | Restart policy for all containers within the pod.
__*Optional*__ +**serviceAccount**?🔹 | [IServiceAccount](#cdk8s-plus-iserviceaccount) | The service account used to run this pod.
__*Optional*__ + +### Methods + + +#### addContainer(container)🔹 + +Add a container to the pod. + +```ts +addContainer(container: Container): void +``` + +* **container** ([Container](#cdk8s-plus-container)) The container. + + + + +#### addVolume(volume)🔹 + +Add a volume to the pod. + +```ts +addVolume(volume: Volume): void +``` + +* **volume** ([Volume](#cdk8s-plus-volume)) The volume. + + + + + + ## interface IResource 🔹 __Implemented by__: [ConfigMap](#cdk8s-plus-configmap), [Deployment](#cdk8s-plus-deployment), [Ingress](#cdk8s-plus-ingress), [Job](#cdk8s-plus-job), [Pod](#cdk8s-plus-pod), [Secret](#cdk8s-plus-secret), [Service](#cdk8s-plus-service), [ServiceAccount](#cdk8s-plus-serviceaccount) @@ -1882,23 +2016,13 @@ Properties for initialization of `Job`. Name | Type | Description -----|------|------------- +**containers**?🔹 | Array<[Container](#cdk8s-plus-container)> | List of containers belonging to the pod.
__*Default*__: No containers. Note that a pod spec must include at least one container. **metadata**?🔹 | [ApiObjectMetadata](#cdk8s-apiobjectmetadata) | Metadata that all persisted resources must have, which includes all objects users must create.
__*Optional*__ -**spec**?🔹 | [JobSpec](#cdk8s-plus-jobspec) | The spec of the job.
__*Default*__: An empty spec will be created. - - - -## struct JobSpec 🔹 - - -Properties for initialization of `JobSpec`. - - - -Name | Type | Description ------|------|------------- -**podMetadataTemplate**?🔹 | [ApiObjectMetadata](#cdk8s-apiobjectmetadata) | The metadata of pods created by this job.
__*Optional*__ -**podSpecTemplate**?🔹 | [PodSpec](#cdk8s-plus-podspec) | The spec of pods created by this job.
__*Optional*__ +**podMetadata**?🔹 | [ApiObjectMetadata](#cdk8s-apiobjectmetadata) | The pod metadata.
__*Optional*__ +**restartPolicy**?🔹 | [RestartPolicy](#cdk8s-plus-restartpolicy) | Restart policy for all containers within the pod.
__*Default*__: RestartPolicy.ALWAYS +**serviceAccount**?🔹 | [IServiceAccount](#cdk8s-plus-iserviceaccount) | A service account provides an identity for processes that run in a Pod.
__*Default*__: No service account. **ttlAfterFinished**?🔹 | [Duration](#cdk8s-plus-duration) | Limits the lifetime of a Job that has finished execution (either Complete or Failed).
__*Default*__: If this field is unset, the Job won't be automatically deleted. +**volumes**?🔹 | Array<[Volume](#cdk8s-plus-volume)> | List of volumes that can be mounted by containers belonging to the pod.
__*Default*__: No volumes. @@ -1941,15 +2065,18 @@ Properties for initialization of `Pod`. Name | Type | Description -----|------|------------- +**containers**?🔹 | Array<[Container](#cdk8s-plus-container)> | List of containers belonging to the pod.
__*Default*__: No containers. Note that a pod spec must include at least one container. **metadata**?🔹 | [ApiObjectMetadata](#cdk8s-apiobjectmetadata) | Metadata that all persisted resources must have, which includes all objects users must create.
__*Optional*__ -**spec**?🔹 | [PodSpec](#cdk8s-plus-podspec) | The spec of the pod.
__*Default*__: An empty spec will be created. +**restartPolicy**?🔹 | [RestartPolicy](#cdk8s-plus-restartpolicy) | Restart policy for all containers within the pod.
__*Default*__: RestartPolicy.ALWAYS +**serviceAccount**?🔹 | [IServiceAccount](#cdk8s-plus-iserviceaccount) | A service account provides an identity for processes that run in a Pod.
__*Default*__: No service account. +**volumes**?🔹 | Array<[Volume](#cdk8s-plus-volume)> | List of volumes that can be mounted by containers belonging to the pod.
__*Default*__: No volumes. -## struct PodSpec 🔹 +## struct PodSpecProps 🔹 -Properties for initialization of `PodSpec`. +Properties of a `PodSpec`. @@ -1962,6 +2089,25 @@ Name | Type | Description +## struct PodTemplateProps 🔹 + + +Properties of a `PodTemplate`. + +Adds metadata information on top of the spec. + + + +Name | Type | Description +-----|------|------------- +**containers**?🔹 | Array<[Container](#cdk8s-plus-container)> | List of containers belonging to the pod.
__*Default*__: No containers. Note that a pod spec must include at least one container. +**podMetadata**?🔹 | [ApiObjectMetadata](#cdk8s-apiobjectmetadata) | The pod metadata.
__*Optional*__ +**restartPolicy**?🔹 | [RestartPolicy](#cdk8s-plus-restartpolicy) | Restart policy for all containers within the pod.
__*Default*__: RestartPolicy.ALWAYS +**serviceAccount**?🔹 | [IServiceAccount](#cdk8s-plus-iserviceaccount) | A service account provides an identity for processes that run in a Pod.
__*Default*__: No service account. +**volumes**?🔹 | Array<[Volume](#cdk8s-plus-volume)> | List of volumes that can be mounted by containers belonging to the pod.
__*Default*__: No volumes. + + + ## struct ResourceProps 🔹 @@ -2058,24 +2204,11 @@ Properties for initialization of `Service`. -Name | Type | Description ------|------|------------- -**metadata**?🔹 | [ApiObjectMetadata](#cdk8s-apiobjectmetadata) | Metadata that all persisted resources must have, which includes all objects users must create.
__*Optional*__ -**spec**?🔹 | [ServiceSpec](#cdk8s-plus-servicespec) | The spec of the service.
__*Default*__: An empty spec will be created. - - - -## struct ServiceSpec 🔹 - - -Properties for initialization of `ServiceSpec`. - - - Name | Type | Description -----|------|------------- **clusterIP**?🔹 | string | The IP address of the service and is usually assigned randomly by the master.
__*Default*__: Automatically assigned. **externalIPs**?🔹 | Array | A list of IP addresses for which nodes in the cluster will also accept traffic for this service.
__*Default*__: No external IPs. +**metadata**?🔹 | [ApiObjectMetadata](#cdk8s-apiobjectmetadata) | Metadata that all persisted resources must have, which includes all objects users must create.
__*Optional*__ **ports**?🔹 | Array<[ServicePort](#cdk8s-plus-serviceport)> | The port exposed by this service.
__*Optional*__ **type**?🔹 | [ServiceType](#cdk8s-plus-servicetype) | Determines how the Service is exposed.
__*Default*__: ServiceType.ClusterIP diff --git a/packages/cdk8s-plus/README.md b/packages/cdk8s-plus/README.md index 94d0277b92..92fe0a882e 100644 --- a/packages/cdk8s-plus/README.md +++ b/packages/cdk8s-plus/README.md @@ -57,16 +57,12 @@ container.mount(appPath, appVolume); // now lets create a deployment to run a few instances of this container const deployment = new kplus.Deployment(chart, 'Deployment', { - spec: { - replicas: 3, - podSpecTemplate: { - containers: [ container ] - } - }, + replicas: 3, + containers: [ container ] }); // finally, we expose the deployment as a load balancer service and make it run -deployment.expose({port: 8080, serviceType: kplus.ServiceType.LOAD_BALANCER}) +deployment.expose(8080, {serviceType: kplus.ServiceType.LOAD_BALANCER}) // we are done, synth app.synth(); @@ -161,14 +157,10 @@ const app = new cdk8s.App(); const chart = new cdk8s.Chart(app, 'Chart'); new kplus.Deployment(chart, 'Deployment', { - spec: { - replicas: 3, - podSpecTemplate: { - containers: [new kplus.Container({ - image: 'ubuntu', - })], - }, - }, + replicas: 3, + containers: [new kplus.Container({ + image: 'ubuntu', + })], }); ``` @@ -184,10 +176,8 @@ app = cdk8s.App() chart = cdk8s.Chart(app, 'Chart') kplus.Deployment(chart, 'Deployment', - spec=kplus.DeploymentSpec( - replicas=1, - pod_spec_template=kplus.PodSpec(containers=[kplus.Container(image='ubuntu')]) - ) + replicas=1, + containers=[kplus.Container(image='ubuntu')] ) ``` @@ -324,21 +314,19 @@ You can configure a TTL for the job after it finished its execution successfully import * as k from 'cdk8s'; import * as kplus from 'cdk8s-plus'; +const app = new k.App(); +const chart = new k.Chart(app, 'Chart'); + // let's define a job spec, and set a 1 second TTL. -const jobSpec = { - ttlAfterFinished: kplus.Duration.seconds(1), -}; +const load = new kplus.Job(chart, 'LoadData', { + ttlAfterFinished: kplus.Duration.seconds(1) + }); + // now add a container to all the pods created by this job -jobSpec.podSpecTemplate.addContainer(new kplus.Container({ +job.addContainer(new kplus.Container({ image: 'loader' })); - -const app = new k.App(); -const chart = new k.Chart(app, 'Chart'); - -// now we create the job -const load = new kplus.Job(chart, 'LoadData', { spec: jobSpec }); ``` ### `Service` @@ -362,7 +350,7 @@ const chart = new k.Chart(app, 'Chart'); const frontends = new kplus.Service(chart, 'FrontEnds'); // this will cause the service to select all pods with the 'run: frontend' label. -frontends.spec.selectByLabel('run', 'frontend') +frontends.selectByLabel('run', 'frontend') ``` #### Ports @@ -378,7 +366,7 @@ const chart = new k.Chart(app, 'Chart'); const frontends = new kplus.Service(chart, 'FrontEnds'); // make the service bind to port 9000 and redirect to port 80 on the associated containers. -frontends.spec.serve({port: 9000, targetPort: 80) +frontends.serve({port: 9000, targetPort: 80) ``` ### `Deployment` @@ -400,11 +388,7 @@ const app = new k.App(); const chart = new k.Chart(app, 'Chart'); new kplus.Deployment(chart, 'FrontEnds', { - spec: { - podSpecTemplate: { - containers: [ new kplus.Container({ image: 'node' }) ], - } - }, + containers: [ new kplus.Container({ image: 'node' }) ], }); ``` @@ -440,7 +424,7 @@ Following up on pod selection, you can also easily create a service that will se const frontends = new kplus.Deployment(chart, 'FrontEnds'); // create a ClusterIP service that listens on port 9000 and redirects to port 9000 on the containers. -frontends.expose({port: 9000}) +frontends.expose(9000) ``` Notice the resulting manifest, will have the same `cdk8s.deployment` magic label as the selector. @@ -558,10 +542,10 @@ const chart = new k.Chart(app, 'Chart'); const pod = new new kplus.Pod(chart, 'Pod'); // this will automatically add the volume as well. -pod.spec.addContainer(container); +pod.addContainer(container); // but if you want to explicitly add it, simply use: -pod.spec.addVolume(storage); +pod.addVolume(storage); ``` @@ -573,8 +557,9 @@ import * as kplus from 'cdk8s-plus'; const app = new k.App(); const chart = new k.Chart(app, 'Chart'); -const pod = new new kplus.Pod(chart, 'Pod'); -pod.spec.restartPolicy = kplus.RestartPolicy.NEVER; +const pod = new new kplus.Pod(chart, 'Pod', { + restartPolicy: kplus.RestartPolicy.NEVER, +}); ``` #### Assigning a ServiceAccount @@ -585,8 +570,9 @@ import * as kplus from 'cdk8s-plus'; const app = new k.App(); const chart = new k.Chart(app, 'Chart'); -const pod = new new kplus.Pod(chart, 'Pod'); -pod.spec.serviceAccount = kplus.ServiceAccount.fromServiceAccountName('aws'); +const pod = new new kplus.Pod(chart, 'Pod', { + serviceAccount: kplus.ServiceAccount.fromServiceAccountName('aws'), +}); ``` ### `Secret` @@ -676,19 +662,15 @@ to a service associated with a deployment of the ```ts const helloDeployment = new kplus.Deployment(this, text, { - spec: { - podSpecTemplate: { - containers: [ - new kplus.Container({ - image: 'hashicorp/http-echo', - args: [ '-text', 'hello ingress' ] - }) - ] - } - } + containers: [ + new kplus.Container({ + image: 'hashicorp/http-echo', + args: [ '-text', 'hello ingress' ] + }) + ] }); -const helloService = helloDeployment.expose({ port: 5678 }); +const helloService = helloDeployment.expose(5678); const ingress = new Ingress(this, 'ingress'); ingress.addRule('/hello', kplus.IngressBackend.fromService(helloService)); diff --git a/packages/cdk8s-plus/src/config-map.ts b/packages/cdk8s-plus/src/config-map.ts index 1d72422119..9a31f60a00 100644 --- a/packages/cdk8s-plus/src/config-map.ts +++ b/packages/cdk8s-plus/src/config-map.ts @@ -56,13 +56,16 @@ export class ConfigMap extends Resource implements IConfigMap { return { name }; } + /** + * @see base.Resource.apiObject + */ protected readonly apiObject: cdk8s.ApiObject; private readonly _binaryData: { [key: string]: string } = { }; private readonly _data: { [key: string]: string } = { }; public constructor(scope: Construct, id: string, props: ConfigMapProps = { }) { - super(scope, id, props); + super(scope, id, { metadata: props.metadata }); this.apiObject = new k8s.ConfigMap(this, 'ConfigMap', { metadata: props.metadata, diff --git a/packages/cdk8s-plus/src/deployment.ts b/packages/cdk8s-plus/src/deployment.ts index fcdfcc07eb..ea1d7a239f 100644 --- a/packages/cdk8s-plus/src/deployment.ts +++ b/packages/cdk8s-plus/src/deployment.ts @@ -3,29 +3,34 @@ import { Construct, Node } from 'constructs'; import { Service, ServiceType } from './service'; import { Resource, ResourceProps } from './base'; import * as cdk8s from 'cdk8s'; -import { PodSpecDefinition, PodSpec } from './pod'; -import { ApiObjectMetadata, ApiObjectMetadataDefinition, Names } from 'cdk8s'; +import { ApiObjectMetadataDefinition, Names } from 'cdk8s'; +import { RestartPolicy, PodTemplate, IPodTemplate, PodTemplateProps } from './pod' +import { Volume } from './volume'; +import { Container } from './container'; +import { IServiceAccount } from './service-account'; /** * Properties for initialization of `Deployment`. */ -export interface DeploymentProps extends ResourceProps { +export interface DeploymentProps extends ResourceProps, PodTemplateProps { + /** - * The spec of the deployment. Use `deployment.spec` to apply post instatiation mutations. + * Number of desired pods. * - * @default - An empty spec will be created. + * @default 1 */ - readonly spec?: DeploymentSpec; + readonly replicas?: number; /** * Automatically allocates a pod selector for this deployment. * * If this is set to `false` you must define your selector through - * `podSepcTemplate.addLabel()` and `selectByLabel()`. + * `deployment.podMetadata.addLabel()` and `deployment.selectByLabel()`. * * @default true */ readonly defaultSelector?: boolean; + } /** @@ -68,110 +73,68 @@ export interface ExposeOptions { * - Clean up older ReplicaSets that you don't need anymore. * **/ -export class Deployment extends Resource { +export class Deployment extends Resource implements IPodTemplate { + /** - * @see base.Resource.apiObject + * Number of desired pods. */ - protected readonly apiObject: cdk8s.ApiObject; + public readonly replicas: number; /** - * Provides access to the underlying spec. - * - * You can use this field to apply post instantiation mutations - * to the spec. + * @see base.Resource.apiObject */ - public readonly spec: DeploymentSpecDefinition; + protected readonly apiObject: cdk8s.ApiObject; - constructor(scope: Construct, id: string, props: DeploymentProps = {}) { - super(scope, id, props); + private readonly _podTemplate: PodTemplate; + private readonly _labelSelector: Record; - this.spec = new DeploymentSpecDefinition(props.spec); + constructor(scope: Construct, id: string, props: DeploymentProps = {}) { + super(scope, id, { metadata: props.metadata }); - this.apiObject = new k8s.Deployment(this, 'Pod', { + this.apiObject = new k8s.Deployment(this, 'Deployment', { metadata: props.metadata, - spec: cdk8s.Lazy.any({ produce: () => this.spec._toKube() }), + spec: cdk8s.Lazy.any({ produce: () => this._toKube() }), }); + this.replicas = props.replicas ?? 1; + this._podTemplate = new PodTemplate(props); + this._labelSelector = {}; + if (props.defaultSelector ?? true) { const selector = 'cdk8s.deployment'; const matcher = Names.toLabelValue(Node.of(this).path); - this.spec.podMetadataTemplate.addLabel(selector, matcher); - this.spec.selectByLabel(selector, matcher); + this.podMetadata.addLabel(selector, matcher); + this.selectByLabel(selector, matcher); } } - /** - * Expose a deployment via a service. - * - * This is equivalent to running `kubectl expose deployment `. - * - * @param port The port number the service will bind to. - * @param options Options. - */ - public expose(port: number, options: ExposeOptions = {}): Service { - const service = new Service(this, 'Service', { - spec: { - type: options.serviceType ?? ServiceType.CLUSTER_IP, - }, - }); - - service.addDeployment(this, port); - return service; + public get podMetadata(): ApiObjectMetadataDefinition { + return this._podTemplate.podMetadata; } -} - -/** - * Properties for initialization of `DeploymentSpec`. - */ -export interface DeploymentSpec { /** - * Number of desired pods. - * @default 1 - */ - readonly replicas?: number; - - /** - * Template for pod specs. - */ - readonly podSpecTemplate?: PodSpec; - - /** - * Template for pod metadata. - */ - readonly podMetadataTemplate?: ApiObjectMetadata; -} - -/** - * DeploymentSpec is the specification of the desired behavior of the Deployment. - */ -export class DeploymentSpecDefinition { - /** - * Number of desired pods. - */ - public readonly replicas?: number; - - /** - * Provides access to the underlying pod template spec. + * The labels this deployment will match against in order to select pods. * - * You can use this field to apply post instatiation mutations - * to the spec. + * Returns a a copy. Use `selectByLabel()` to add labels. */ - public readonly podSpecTemplate: PodSpecDefinition; + public get labelSelector(): Record { + return { ...this._labelSelector }; + } - /** - * Template for pod metadata. - */ - public readonly podMetadataTemplate: ApiObjectMetadataDefinition; + public get containers(): Container[] { + return this._podTemplate.containers; + } - private readonly _labelSelector: Record; + public get volumes(): Volume[] { + return this._podTemplate.volumes; + } - constructor(props: DeploymentSpec = {}) { - this.replicas = props.replicas ?? 1; - this.podSpecTemplate = new PodSpecDefinition(props.podSpecTemplate); - this.podMetadataTemplate = new ApiObjectMetadataDefinition(props.podMetadataTemplate); + public get restartPolicy(): RestartPolicy | undefined { + return this._podTemplate.restartPolicy; + } - this._labelSelector = {}; + public get serviceAccount(): IServiceAccount | undefined { + return this._podTemplate.serviceAccount; } /** @@ -186,27 +149,42 @@ export class DeploymentSpecDefinition { } /** - * The labels this deployment will match against in order to select pods. + * Expose a deployment via a service. * - * Returns a a copy. Use `selectByLabel()` to add labels. + * This is equivalent to running `kubectl expose deployment `. + * + * @param port The port number the service will bind to. + * @param options Options. */ - public get labelSelector(): Record { - return { ...this._labelSelector }; + public expose(port: number, options: ExposeOptions = {}): Service { + const service = new Service(this, 'Service', { + type: options.serviceType ?? ServiceType.CLUSTER_IP, + }); + + service.addDeployment(this, port); + return service; + } + + public addContainer(container: Container): void { + return this._podTemplate.addContainer(container); + } + + public addVolume(volume: Volume): void { + return this._podTemplate.addVolume(volume); } - + + /** * @internal */ public _toKube(): k8s.DeploymentSpec { return { replicas: this.replicas, - template: { - metadata: this.podMetadataTemplate.toJson(), - spec: this.podSpecTemplate._toKube(), - }, + template: this._podTemplate._toPodTemplateSpec(), selector: { matchLabels: this._labelSelector, }, }; } -} + +} \ No newline at end of file diff --git a/packages/cdk8s-plus/src/index.ts b/packages/cdk8s-plus/src/index.ts index 84a50b2721..e39fcd903e 100644 --- a/packages/cdk8s-plus/src/index.ts +++ b/packages/cdk8s-plus/src/index.ts @@ -10,4 +10,4 @@ export * from './service-account'; export * from './service'; export * from './volume'; export * from './size'; -export * from './ingress'; \ No newline at end of file +export * from './ingress'; diff --git a/packages/cdk8s-plus/src/ingress.ts b/packages/cdk8s-plus/src/ingress.ts index 8700f94fd7..5e9c8c22da 100644 --- a/packages/cdk8s-plus/src/ingress.ts +++ b/packages/cdk8s-plus/src/ingress.ts @@ -9,7 +9,7 @@ import { Service } from './service'; */ export interface IngressProps extends ResourceProps { /** - * The default backend services requests that do not match any rule. + * The default backend services requests that do not match any rule. * * Using this option or the `addDefaultBackend()` method is equivalent to * adding a rule with both `path` and `host` undefined. @@ -36,13 +36,17 @@ export interface IngressProps extends ResourceProps { * based virtual hosting etc. */ export class Ingress extends Resource { + + /** + * @see base.Resource.apiObject + */ protected readonly apiObject: ApiObject; private readonly _rulesPerHost: { [host: string]: k8s.HttpIngressPath[] } = {}; private _defaultBackend?: IngressBackend; constructor(scope: Construct, id: string, props: IngressProps = {}) { - super(scope, id, props); + super(scope, id, { metadata: props.metadata }); this.apiObject = new k8s.Ingress(this, 'Ingress', { metadata: props.metadata, @@ -69,7 +73,7 @@ export class Ingress extends Resource { /** * Defines the default backend for this ingress. A default backend capable of * servicing requests that don't match any rule. - * + * * @param backend The backend to use for requests that do not match any rule. */ public addDefaultBackend(backend: IngressBackend) { @@ -79,7 +83,7 @@ export class Ingress extends Resource { /** * Specify a default backend for a specific host name. This backend will be used as a catch-all for requests * targeted to this host name (the `Host` header matches this value). - * + * * @param host The host name to match * @param backend The backend to route to */ @@ -117,7 +121,7 @@ export class Ingress extends Resource { */ public addRules(...rules: IngressRule[]) { for (const rule of rules) { - + // default backend is not really a rule if (!rule.host && !rule.path) { if (this._defaultBackend) { @@ -134,7 +138,7 @@ export class Ingress extends Resource { if (path && !path.startsWith('/')) { throw new Error(`ingress paths must begin with a "/": ${path}`); } - + const routes = this._rulesPerHost[host] = this._rulesPerHost[host] ?? []; // check if we already have a rule for this host/path @@ -148,7 +152,7 @@ export class Ingress extends Resource { private synthRules(): undefined | k8s.IngressRule[] { const rules = new Array(); - + for (const [ host, paths ] of Object.entries(this._rulesPerHost)) { rules.push({ host: host ? host : undefined, @@ -165,7 +169,7 @@ export class Ingress extends Resource { */ export interface ServiceIngressBackendOptions { /** - * The port to use to access the service. + * The port to use to access the service. * * - This option will fail if the service does not expose any ports. * - If the service exposes multiple ports, this option must be specified. @@ -186,23 +190,23 @@ export class IngressBackend { * @param service The service object. */ public static fromService(service: Service, options: ServiceIngressBackendOptions = {}) { - if (service.spec.ports.length === 0) { + if (service.ports.length === 0) { throw new Error('service does not expose any ports'); } let servicePort; - if (service.spec.ports.length === 1) { - servicePort = service.spec.ports[0].port; + if (service.ports.length === 1) { + servicePort = service.ports[0].port; } else { if (options.port !== undefined) { - const found = service.spec.ports.find(p => p.port === options.port); + const found = service.ports.find(p => p.port === options.port); if (found) { servicePort = found.port; } else { - throw new Error(`service exposes ports ${service.spec.ports.map(p => p.port).join(',')} but backend is defined to use port ${options.port}`); + throw new Error(`service exposes ports ${service.ports.map(p => p.port).join(',')} but backend is defined to use port ${options.port}`); } } else { - throw new Error(`unable to determine service port since service exposes multiple ports: ${service.spec.ports.map(x => x.port).join(',')}`); + throw new Error(`unable to determine service port since service exposes multiple ports: ${service.ports.map(x => x.port).join(',')}`); } } @@ -247,7 +251,7 @@ export interface IngressRule { * port of an Ingress is implicitly :80 for http and :443 for https. Both * these may change in the future. Incoming requests are matched against the * host before the IngressRuleValue. - * + * * @default - If the host is unspecified, the Ingress routes all traffic based * on the specified IngressRuleValue. */ diff --git a/packages/cdk8s-plus/src/job.ts b/packages/cdk8s-plus/src/job.ts index 2e88f2eab8..04eb3387ac 100644 --- a/packages/cdk8s-plus/src/job.ts +++ b/packages/cdk8s-plus/src/job.ts @@ -1,24 +1,33 @@ import { Resource, ResourceProps } from './base'; -import { ApiObject, ApiObjectMetadata, ApiObjectMetadataDefinition } from 'cdk8s'; +import { ApiObject, ApiObjectMetadataDefinition } from 'cdk8s'; import { Construct } from 'constructs'; import * as cdk8s from 'cdk8s'; - import * as k8s from './imports/k8s'; -import { RestartPolicy, PodSpec, PodSpecDefinition } from './pod'; +import { RestartPolicy, PodTemplateProps, IPodTemplate, PodTemplate } from './pod'; import { Duration } from './duration'; +import { Container } from './container'; +import { IServiceAccount } from './service-account'; +import { Volume } from './volume'; /** * Properties for initialization of `Job`. */ -export interface JobProps extends ResourceProps { +export interface JobProps extends ResourceProps, PodTemplateProps { /** - * The spec of the job. Use `job.spec` to apply post instantiation mutations. + * Limits the lifetime of a Job that has finished execution (either Complete + * or Failed). If this field is set, after the Job finishes, it is eligible to + * be automatically deleted. When the Job is being deleted, its lifecycle + * guarantees (e.g. finalizers) will be honored. If this field is set to zero, + * the Job becomes eligible to be deleted immediately after it finishes. This + * field is alpha-level and is only honored by servers that enable the + * `TTLAfterFinished` feature. * - * @default - An empty spec will be created. + * @default - If this field is unset, the Job won't be automatically deleted. */ - readonly spec?: JobSpec; + readonly ttlAfterFinished?: Duration; + } /** @@ -28,74 +37,63 @@ export interface JobProps extends ResourceProps { * The Job object will start a new Pod if the first Pod fails or is deleted (for example due to a node hardware failure or a node reboot). * You can also use a Job to run multiple Pods in parallel. */ -export class Job extends Resource { +export class Job extends Resource implements IPodTemplate { + + /** + * TTL before the job is deleted after it is finished. + */ + public readonly ttlAfterFinished?: Duration; + + /** + * @see base.Resource.apiObject + */ protected readonly apiObject: ApiObject; - public readonly spec: JobSpecDefinition; - constructor(scope: Construct, id: string, props: JobProps = {}) { - super(scope, id, props); + private readonly _podTemplate: PodTemplate; - this.spec = new JobSpecDefinition(props.spec); + constructor(scope: Construct, id: string, props: JobProps = {}) { + super(scope, id, { metadata: props.metadata }); this.apiObject = new k8s.Job(this, 'Default', { metadata: props.metadata, - spec: cdk8s.Lazy.any({ produce: () => this.spec._toKube() }), + spec: cdk8s.Lazy.any({ produce: () => this._toKube() }), }); + + this._podTemplate = new PodTemplate({ + ...props, + restartPolicy: props.restartPolicy ?? RestartPolicy.NEVER, + }) + this.ttlAfterFinished = props.ttlAfterFinished; + } -} -/** - * Properties for initialization of `JobSpec`. - */ -export interface JobSpec { - /** - * The spec of pods created by this job. - */ - readonly podSpecTemplate?: PodSpec; + public get podMetadata(): ApiObjectMetadataDefinition { + return this._podTemplate.podMetadata; + } - /** - * The metadata of pods created by this job. - */ - readonly podMetadataTemplate?: ApiObjectMetadata; + public get containers(): Container[] { + return this._podTemplate.containers; + } - /** - * Limits the lifetime of a Job that has finished execution (either Complete - * or Failed). If this field is set, after the Job finishes, it is eligible to - * be automatically deleted. When the Job is being deleted, its lifecycle - * guarantees (e.g. finalizers) will be honored. If this field is set to zero, - * the Job becomes eligible to be deleted immediately after it finishes. This - * field is alpha-level and is only honored by servers that enable the - * `TTLAfterFinished` feature. - * - * @default - If this field is unset, the Job won't be automatically deleted. - */ - readonly ttlAfterFinished?: Duration; -} + public get volumes(): Volume[] { + return this._podTemplate.volumes; + } -export class JobSpecDefinition { - /** - * The spec for pods created by this job. - */ - public readonly podSpecTemplate: PodSpecDefinition; + public get restartPolicy(): RestartPolicy | undefined { + return this._podTemplate.restartPolicy; + } - /** - * The metadata for pods created by this job. - */ - public readonly podMetadataTemplate: ApiObjectMetadataDefinition; + public get serviceAccount(): IServiceAccount | undefined { + return this._podTemplate.serviceAccount; + } - /** - * TTL before the job is deleted after it is finished. - */ - public readonly ttlAfterFinished?: Duration; + public addContainer(container: Container): void { + return this._podTemplate.addContainer(container); + } - constructor(props: JobSpec = {}) { - this.podSpecTemplate = new PodSpecDefinition({ - restartPolicy: props.podSpecTemplate?.restartPolicy ?? RestartPolicy.NEVER, - ...props.podSpecTemplate, - }); - this.podMetadataTemplate = new ApiObjectMetadataDefinition(props.podMetadataTemplate); - this.ttlAfterFinished = props.ttlAfterFinished; + public addVolume(volume: Volume): void { + return this._podTemplate.addVolume(volume); } /** @@ -103,11 +101,9 @@ export class JobSpecDefinition { */ public _toKube(): k8s.JobSpec { return { - template: { - metadata: this.podMetadataTemplate.toJson(), - spec: this.podSpecTemplate._toKube(), - }, + template: this._podTemplate._toPodTemplateSpec(), ttlSecondsAfterFinished: this.ttlAfterFinished ? this.ttlAfterFinished.toSeconds() : undefined, }; } -} \ No newline at end of file + +} diff --git a/packages/cdk8s-plus/src/pod.ts b/packages/cdk8s-plus/src/pod.ts index 233894a1a0..5af5d2e1c9 100644 --- a/packages/cdk8s-plus/src/pod.ts +++ b/packages/cdk8s-plus/src/pod.ts @@ -5,52 +5,187 @@ import * as cdk8s from 'cdk8s'; import { IServiceAccount } from './service-account'; import { Container } from './container'; import { Volume } from './volume'; +import { ApiObjectMetadata, ApiObjectMetadataDefinition } from 'cdk8s'; /** - * Properties for initialization of `Pod`. + * Represents a resource that can be configured with a kuberenets pod spec. (e.g `Deployment`, `Job`, `Pod`, ...). + * + * Use the `PodSpec` class as an implementation helper. */ -export interface PodProps extends ResourceProps { +export interface IPodSpec { + + /** + * The containers belonging to the pod. + * + * Use `addContainer` to add containers. + */ + readonly containers: Container[]; /** - * The spec of the pod. Use `pod.spec` to apply post instantiation mutations. + * The volumes associated with this pod. * - * @default - An empty spec will be created. + * Use `addVolume` to add volumes. */ - readonly spec?: PodSpec; + readonly volumes: Volume[]; + + /** + * Restart policy for all containers within the pod. + */ + readonly restartPolicy?: RestartPolicy; + + /** + * The service account used to run this pod. + */ + readonly serviceAccount?: IServiceAccount; + + /** + * Add a container to the pod. + * + * @param container The container. + */ + addContainer(container: Container): void; + + /** + * Add a volume to the pod. + * + * @param volume The volume. + */ + addVolume(volume: Volume): void; } /** - * Pod is a collection of containers that can run on a host. This resource is - * created by clients and scheduled onto hosts. + * Represents a resource that can be configured with a kuberenets pod template. (e.g `Deployment`, `Job`, ...). + * + * Use the `PodTemplate` class as an implementation helper. */ -export class Pod extends Resource { - protected readonly apiObject: cdk8s.ApiObject; +export interface IPodTemplate extends IPodSpec { /** - * Provides access to the underlying spec. - * - * You can use this field to apply post instantiation mutations - * to the spec. + * Provides read/write access to the underlying pod metadata of the resource. */ - public readonly spec: PodSpecDefinition; + readonly podMetadata: ApiObjectMetadataDefinition; +} - constructor(scope: Construct, id: string, props: PodProps = {}) { - super(scope, id, props); +/** + * Provides read/write capabilities ontop of a `PodSpecProps`. + */ +export class PodSpec implements IPodSpec { - this.spec = new PodSpecDefinition(props.spec); + public readonly restartPolicy?: RestartPolicy; + public readonly serviceAccount?: IServiceAccount; - this.apiObject = new k8s.Pod(this, 'Pod', { - metadata: props.metadata, - spec: cdk8s.Lazy.any({ produce: () => this.spec._toKube() }), - }); + private readonly _containers: Container[]; + private readonly _volumes: Volume[]; + + constructor(props: PodSpecProps = {}) { + this.restartPolicy = props.restartPolicy; + this.serviceAccount = props.serviceAccount; + + this._containers = props.containers ?? []; + this._volumes = props.volumes ?? []; + } + + public get containers(): Container[] { + return [ ...this._containers ]; + } + + public get volumes(): Volume[] { + return [ ...this._volumes ]; + } + + public addContainer(container: Container): void { + this._containers.push(container); + } + + public addVolume(volume: Volume): void { + this._volumes.push(volume); } + + /** + * @internal + */ + public _toPodSpec(): k8s.PodSpec { + + if (this.containers.length === 0) { + throw new Error('PodSpec must have at least 1 container'); + } + + const volumes: k8s.Volume[] = []; + const containers: k8s.Container[] = []; + + for (const container of this.containers) { + + // automatically add volume from the container mount + // to this pod so thats its available to the container. + for (const mount of container.mounts) { + volumes.push(mount.volume._toKube()); + } + + containers.push(container._toKube()); + } + + for (const volume of this._volumes) { + volumes.push(volume._toKube()); + } + + return { + restartPolicy: this.restartPolicy, + serviceAccountName: this.serviceAccount?.name, + containers: containers, + volumes: volumes, + }; + + } + +} + +/** + * Properties of a `PodTemplate`. + * + * Adds metadata information on top of the spec. + */ +export interface PodTemplateProps extends PodSpecProps { + + /** + * The pod metadata. + */ + readonly podMetadata?: ApiObjectMetadata; } + /** - * Properties for initialization of `PodSpec`. + * Provides read/write capabilities ontop of a `PodTemplateProps`. */ -export interface PodSpec { +export class PodTemplate extends PodSpec implements IPodTemplate { + + public readonly podMetadata: ApiObjectMetadataDefinition; + + constructor(props: PodTemplateProps = {}) { + super(props); + this.podMetadata = new ApiObjectMetadataDefinition(props.podMetadata); + } + + /** + * @internal + */ + public _toPodTemplateSpec(): k8s.PodTemplateSpec { + return { + metadata: this.podMetadata.toJson(), + spec: this._toPodSpec(), + } + } +} + +/** + * Properties for initialization of `Pod`. + */ +export interface PodProps extends ResourceProps, PodSpecProps {} + +/** + * Properties of a `PodSpec`. + */ +export interface PodSpecProps { /** * List of containers belonging to the pod. Containers cannot currently be @@ -97,121 +232,76 @@ export interface PodSpec { * @default - No service account. */ readonly serviceAccount?: IServiceAccount; + } /** - * Restart policy for all containers within the pod. + * Pod is a collection of containers that can run on a host. This resource is + * created by clients and scheduled onto hosts. */ -export enum RestartPolicy { - /** - * Always restart the pod after it exits. - */ - ALWAYS = 'Always', +export class Pod extends Resource implements IPodSpec { /** - * Only restart if the pod exits with a non-zero exit code. + * @see base.Resource.apiObject */ - ON_FAILURE = 'OnFailure', + protected readonly apiObject: cdk8s.ApiObject; - /** - * Never restart the pod. - */ - NEVER = 'Never' -} + private readonly _spec: PodSpec; -/** - * A description of a pod. - */ -export class PodSpecDefinition { - /** - * Restart policy for all containers within the pod. - */ - public readonly restartPolicy?: RestartPolicy; - - /** - * The service account used to run this pod. - */ - public readonly serviceAccount?: IServiceAccount; + constructor(scope: Construct, id: string, props: PodProps = {}) { + super(scope, id, { metadata: props.metadata }); - private readonly _containers: Container[]; - private readonly _volumes: Volume[]; + this.apiObject = new k8s.Pod(this, 'Pod', { + metadata: props.metadata, + spec: cdk8s.Lazy.any({ produce: () => this._spec._toPodSpec() }), + }); - constructor(props: PodSpec = {}) { - this._containers = props.containers ?? []; - this._volumes = props.volumes ?? []; - this.restartPolicy = props.restartPolicy; - this.serviceAccount = props.serviceAccount; + this._spec = new PodSpec(props); } - /** - * List of containers belonging to the pod. - * - * @returns a copy - do not modify - */ public get containers(): Container[] { - return [ ...this._containers ]; + return this._spec.containers; + } + + public get volumes(): Volume[] { + return this._spec.volumes; + } + + public get restartPolicy(): RestartPolicy | undefined { + return this._spec.restartPolicy; + } + + public get serviceAccount(): IServiceAccount | undefined { + return this._spec.serviceAccount; } - /** - * Adds a container to this pod. - * - * @param container The container to add - */ public addContainer(container: Container): void { - this._containers.push(container); + return this._spec.addContainer(container); } - /** - * Adds a volume to this pod. - * - * @param volume The volume to add - */ public addVolume(volume: Volume): void { - this._volumes.push(volume); + return this._spec.addVolume(volume); } +} + +/** + * Restart policy for all containers within the pod. + */ +export enum RestartPolicy { /** - * List of volumes that can be mounted by containers belonging to the pod. - * - * Returns a copy. To add volumes, use `addVolume()`. + * Always restart the pod after it exits. */ - public get volumes() { - return [ ...this._volumes ]; - } + ALWAYS = 'Always', /** - * @internal + * Only restart if the pod exits with a non-zero exit code. */ - public _toKube(): k8s.PodSpec { - - if (this.containers.length === 0) { - throw new Error('PodSpec must have at least 1 container'); - } - - const volumes: k8s.Volume[] = []; - const containers: k8s.Container[] = []; - - for (const container of this.containers) { - - // automatically add volume from the container mount - // to this pod so thats its available to the container. - for (const mount of container.mounts) { - volumes.push(mount.volume._toKube()); - } - - containers.push(container._toKube()); - } - - for (const volume of this._volumes) { - volumes.push(volume._toKube()); - } - - return { - restartPolicy: this.restartPolicy, - serviceAccountName: this.serviceAccount?.name, - containers: containers, - volumes: volumes, - }; + ON_FAILURE = 'OnFailure', - } + /** + * Never restart the pod. + */ + NEVER = 'Never' } + diff --git a/packages/cdk8s-plus/src/secret.ts b/packages/cdk8s-plus/src/secret.ts index 21ff74b3ef..fd133dc936 100644 --- a/packages/cdk8s-plus/src/secret.ts +++ b/packages/cdk8s-plus/src/secret.ts @@ -35,12 +35,15 @@ export class Secret extends Resource implements ISecret { return { name }; } + /** + * @see base.Resource.apiObject + */ protected readonly apiObject: cdk8s.ApiObject; private readonly stringData: { [key: string]: string }; public constructor(scope: Construct, id: string, props: SecretProps = { }) { - super(scope, id, props); + super(scope, id, { metadata: props.metadata }); this.stringData = props.stringData ?? {}; diff --git a/packages/cdk8s-plus/src/service-account.ts b/packages/cdk8s-plus/src/service-account.ts index 61f83b8b7c..1482e3b4f0 100644 --- a/packages/cdk8s-plus/src/service-account.ts +++ b/packages/cdk8s-plus/src/service-account.ts @@ -53,12 +53,15 @@ export class ServiceAccount extends Resource implements IServiceAccount { return { name: name }; } + /** + * @see base.Resource.apiObject + */ protected readonly apiObject: ApiObject; private readonly _secrets: ISecret[]; constructor(scope: Construct, id: string, props: ServiceAccountProps = { }) { - super(scope, id, props); + super(scope, id, { metadata: props.metadata }); this._secrets = props.secrets ?? []; diff --git a/packages/cdk8s-plus/src/service.ts b/packages/cdk8s-plus/src/service.ts index 3b13d1b039..d8fb1c9ff0 100644 --- a/packages/cdk8s-plus/src/service.ts +++ b/packages/cdk8s-plus/src/service.ts @@ -10,11 +10,46 @@ import { Deployment } from './deployment'; export interface ServiceProps extends ResourceProps { /** - * The spec of the service. Use `service.spec` to apply post instantiation mutations. + * The IP address of the service and is usually assigned randomly by the + * master. If an address is specified manually and is not in use by others, it + * will be allocated to the service; otherwise, creation of the service will + * fail. This field can not be changed through updates. Valid values are + * "None", empty string (""), or a valid IP address. "None" can be specified + * for headless services when proxying is not required. Only applies to types + * ClusterIP, NodePort, and LoadBalancer. Ignored if type is ExternalName. + * + * @see https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies + * @default - Automatically assigned. + * + */ + readonly clusterIP?: string; + + /** + * A list of IP addresses for which nodes in the cluster will also accept + * traffic for this service. These IPs are not managed by Kubernetes. The user + * is responsible for ensuring that traffic arrives at a node with this IP. A + * common example is external load-balancers that are not part of the + * Kubernetes system. + * + * @default - No external IPs. + */ + readonly externalIPs?: string[]; + + /** + * Determines how the Service is exposed. + * + * More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + * + * @default ServiceType.ClusterIP + */ + readonly type?: ServiceType; + + /** + * The port exposed by this service. * - * @default - An empty spec will be created. + * More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies */ - readonly spec?: ServiceSpec; + readonly ports?: ServicePort[]; } @@ -72,25 +107,62 @@ export enum ServiceType { * or load balancer in between your application and the backend Pods. */ export class Service extends Resource { - protected readonly apiObject: cdk8s.ApiObject; /** - * Provides access to the underlying spec. - * - * You can use this field to apply post instantiation mutations - * to the spec. + * The IP address of the service and is usually assigned randomly by the + * master. */ - public readonly spec: ServiceSpecDefinition; + public readonly clusterIP?: string; - constructor(scope: Construct, id: string, props: ServiceProps = {}) { - super(scope, id, props); + /** + * Determines how the Service is exposed. + */ + public readonly type: ServiceType; + + /** + * @see base.Resource.apiObject + */ + protected readonly apiObject: cdk8s.ApiObject; - this.spec = new ServiceSpecDefinition(props.spec); + private readonly _externalIPs: string[]; + private readonly _selector: Record; + private readonly _ports: ServicePort[]; + + constructor(scope: Construct, id: string, props: ServiceProps = {}) { + super(scope, id, { metadata: props.metadata }); this.apiObject = new k8s.Service(this, 'Pod', { metadata: props.metadata, - spec: cdk8s.Lazy.any({ produce: () => this.spec._toKube() }), + spec: cdk8s.Lazy.any({ produce: () => this._toKube() }), }); + + this.clusterIP = props.clusterIP; + this.type = props.type ?? ServiceType.CLUSTER_IP; + + this._externalIPs = props.externalIPs ?? []; + this._ports = []; + this._selector = { }; + + for (const portAndOptions of props.ports ?? []) { + this.serve(portAndOptions.port, portAndOptions); + } + + } + + /** + * Returns the labels which are used to select pods for this service. + */ + public get selector() { + return this._selector; + } + + /** + * Ports for this service. + * + * Use `serve()` to expose additional service ports. + */ + public get ports() { + return [...this._ports]; } /** @@ -104,30 +176,78 @@ export class Service extends Resource { * @param port The external port */ public addDeployment(deployment: Deployment, port: number) { - const containers = deployment.spec.podSpecTemplate.containers; + const containers = deployment.containers; if (containers.length === 0) { throw new Error('Cannot expose a deployment without containers'); } - const selector = Object.entries(deployment.spec.labelSelector); + const selector = Object.entries(deployment.labelSelector); if (selector.length === 0) { throw new Error('deployment does not have a label selector'); } - if (Object.keys(this.spec.selector).length > 0) { + if (Object.keys(this.selector).length > 0) { throw new Error('a selector is already defined for this service. cannot add a deployment'); } for (const [ k, v ] of selector) { - this.spec.addSelector(k, v); + this.addSelector(k, v); } - this.spec.serve(port, { + this.serve(port, { // just a PoC, we assume the first container is the main one. // TODO: figure out what the correct thing to do here. targetPort: containers[0].port, }); - } + } + + /** + * Services defined using this spec will select pods according the provided label. + * + * @param label The label key. + * @param value The label value. + */ + public addSelector(label: string, value: string) { + this._selector[label] = value; + } + + /** + * Configure a port the service will bind to. + * This method can be called multiple times. + * + * @param port The port definition. + */ + public serve(port: number, options: ServicePortOptions = { }) { + this._ports.push({ port, ...options }); + } + + /** + * @internal + */ + public _toKube(): k8s.ServiceSpec { + if (this._ports.length === 0) { + throw new Error('A service must be configured with a port'); + } + + const ports: k8s.ServicePort[] = []; + + for (const port of this._ports) { + ports.push({ + port: port.port, + targetPort: port.targetPort, + nodePort: port.nodePort, + }); + } + + return { + clusterIP: this.clusterIP, + externalIPs: this._externalIPs, + type: this.type, + selector: this._selector, + ports: ports, + }; + } + } export enum Protocol { @@ -184,152 +304,3 @@ export interface ServicePort extends ServicePortOptions { */ readonly port: number; } - -/** - * Properties for initialization of `ServiceSpec`. - */ -export interface ServiceSpec { - - /** - * The IP address of the service and is usually assigned randomly by the - * master. If an address is specified manually and is not in use by others, it - * will be allocated to the service; otherwise, creation of the service will - * fail. This field can not be changed through updates. Valid values are - * "None", empty string (""), or a valid IP address. "None" can be specified - * for headless services when proxying is not required. Only applies to types - * ClusterIP, NodePort, and LoadBalancer. Ignored if type is ExternalName. - * - * @see https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies - * @default - Automatically assigned. - * - */ - readonly clusterIP?: string; - - /** - * A list of IP addresses for which nodes in the cluster will also accept - * traffic for this service. These IPs are not managed by Kubernetes. The user - * is responsible for ensuring that traffic arrives at a node with this IP. A - * common example is external load-balancers that are not part of the - * Kubernetes system. - * - * @default - No external IPs. - */ - readonly externalIPs?: string[]; - - /** - * Determines how the Service is exposed. - * - * More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types - * - * @default ServiceType.ClusterIP - */ - readonly type?: ServiceType; - - /** - * The port exposed by this service. - * - * More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies - */ - readonly ports?: ServicePort[]; -} - -/** - * A description of a service. - */ -export class ServiceSpecDefinition { - /** - * The IP address of the service and is usually assigned randomly by the - * master. - */ - public readonly clusterIP?: string; - - /** - * A list of IP addresses for which nodes in the cluster will also accept - * traffic for this service. - */ - private readonly externalIPs: string[]; - - /** - * Determines how the Service is exposed. - */ - public readonly type: ServiceType; - - private readonly _selector: Record; - - private readonly _ports: ServicePort[]; - - constructor(props: ServiceSpec = {}) { - this.clusterIP = props.clusterIP; - this.externalIPs = props.externalIPs ?? []; - this.type = props.type ?? ServiceType.CLUSTER_IP; - this._ports = []; - this._selector = { }; - - for (const portAndOptions of props.ports ?? []) { - this.serve(portAndOptions.port, portAndOptions); - } - } - - /** - * Returns the labels which are used to select pods for this service. - */ - public get selector() { - return this._selector; - } - - /** - * Services defined using this spec will select pods according the provided label. - * - * @param label The label key. - * @param value The label value. - */ - public addSelector(label: string, value: string) { - this._selector[label] = value; - } - - /** - * Configure a port the service will bind to. - * This method can be called multiple times. - * - * @param port The port definition. - */ - public serve(port: number, options: ServicePortOptions = { }) { - this._ports.push({ port, ...options }); - } - - /** - * Ports for this service. - * - * Use `serve()` to expose additional service ports. - */ - public get ports() { - return [...this._ports]; - } - - /** - * @internal - */ - public _toKube(): k8s.ServiceSpec { - if (this._ports.length === 0) { - throw new Error('A service must be configured with a port'); - } - - const ports: k8s.ServicePort[] = []; - - for (const port of this._ports) { - ports.push({ - port: port.port, - targetPort: port.targetPort, - nodePort: port.nodePort, - }); - } - - return { - clusterIP: this.clusterIP, - externalIPs: this.externalIPs, - type: this.type, - selector: this._selector, - ports: ports, - }; - } -} \ No newline at end of file diff --git a/packages/cdk8s-plus/test/deployment.test.ts b/packages/cdk8s-plus/test/deployment.test.ts index bdd30267c2..9ff9491319 100644 --- a/packages/cdk8s-plus/test/deployment.test.ts +++ b/packages/cdk8s-plus/test/deployment.test.ts @@ -1,190 +1,140 @@ import * as kplus from '../src'; -import * as k8s from '../src/imports/k8s'; -import { Names, Testing } from 'cdk8s'; -import { Node } from 'constructs'; - -describe('DeploymentSpecDefinition', () => { - - test('Instantiation properties are all respected', () => { - const chart = Testing.chart(); - new kplus.Deployment(chart, 'Deployment'); - const spec = new kplus.DeploymentSpecDefinition({ - replicas: 3, - podSpecTemplate: { - serviceAccount: kplus.ServiceAccount.fromServiceAccountName('my-service-account'), - containers: [ - new kplus.Container({ image: 'my-image' }), - ], - }, - }); - - const actual: k8s.DeploymentSpec = spec._toKube(); - - expect(actual.replicas).toEqual(3); - expect(spec.podSpecTemplate.serviceAccount?.name).toBe('my-service-account'); - expect(spec.podSpecTemplate.containers[0].image).toBe('my-image'); - }); +import { Testing } from 'cdk8s'; - test('A label selector is automatically allocated', () => { - // GIVEN - const chart = Testing.chart(); +test('A label selector is automatically allocated', () => { - const d = new kplus.Deployment(chart, 'Deployment'); - d.spec.podSpecTemplate.addContainer(new kplus.Container({ image: 'foobar' })); + const chart = Testing.chart(); - const spec = d.spec._toKube(); + const deployment = new kplus.Deployment(chart, 'Deployment'); + deployment.addContainer(new kplus.Container({ image: 'foobar' })); - const expectedSelector = { 'cdk8s.deployment': 'test-Deployment-9e0110cd' }; - expect(spec.selector.matchLabels).toEqual(expectedSelector); - expect(spec.template.metadata?.labels).toEqual(expectedSelector); - }); + const expectedValue = 'test-Deployment-9e0110cd' + const expectedSelector = { 'cdk8s.deployment': expectedValue }; - test('No selector is generated if "defaultSelector" is false', () => { - // GIVEN - const chart = Testing.chart(); - - // WHEN - const d = new kplus.Deployment(chart, 'Deployment', { - defaultSelector: false, - spec: { - podSpecTemplate: { - containers: [ new kplus.Container({ image: 'foobar' }) ], - }, - }, - }); - - const spec = d.spec._toKube(); - expect(spec.selector.matchLabels).toEqual({}); - expect(spec.template.metadata?.labels).toEqual(undefined); - }); + // assert the k8s spec has it. + const spec = Testing.synth(chart)[0].spec; + expect(spec.selector.matchLabels).toEqual(expectedSelector); + expect(spec.template.metadata?.labels).toEqual(expectedSelector); - test('Can select labels', () => { - const spec = new kplus.DeploymentSpecDefinition(); + // assert the deployment object has it. + expect(deployment.labelSelector).toEqual(expectedSelector); - spec.podSpecTemplate.addContainer( - new kplus.Container({ - image: 'image', - }), - ); +}); + +test('No selector is generated if "defaultSelector" is false', () => { - const chart = Testing.chart(); - new kplus.Deployment(chart, 'Deployment'); + const chart = Testing.chart(); - spec.selectByLabel('key', 'value'); + const deployment = new kplus.Deployment(chart, 'Deployment', { + defaultSelector: false, + containers: [ new kplus.Container({ image: 'foobar' }) ], + }); - const actual: k8s.LabelSelector = spec._toKube().selector; + // assert the k8s spec doesnt have it. + const spec = Testing.synth(chart)[0].spec; + expect(spec.selector.matchLabels).toEqual({}); + expect(spec.template.metadata?.labels).toEqual(undefined); - const expected: k8s.LabelSelector = { - matchLabels: { - key: 'value', - }, - }; + // assert the deployment object doesnt have it. + expect(deployment.labelSelector).toEqual({}); - expect(actual).toEqual(expected); - }); }); -describe('Deployment', () => { - test('Can be exposed as via service', () => { - const chart = Testing.chart(); +test('Can select by label', () => { - const deployment = new kplus.Deployment(chart, 'Deployment'); - deployment.spec.podSpecTemplate.addContainer( + const chart = Testing.chart(); + + const deployment = new kplus.Deployment(chart, 'Deployment', { + containers: [ new kplus.Container({ image: 'image', - port: 9300, }), - ); + ], + defaultSelector: false, + }); - const service = deployment.expose(9200, { - serviceType: kplus.ServiceType.LOAD_BALANCER, - }); + const expectedSelector = { foo: 'bar' }; - const actual = service.spec._toKube(); + deployment.selectByLabel('foo', expectedSelector['foo']); - expect(actual.type).toEqual('LoadBalancer'); - expect(actual.selector).toEqual({ - 'cdk8s.deployment': Names.toLabelValue(Node.of(deployment).path), - }); - expect(actual.ports![0].port).toEqual(9200); - expect(actual.ports![0].targetPort).toEqual(9300); - }); + // assert the k8s spec has it. + const spec = Testing.synth(chart)[0].spec; + expect(spec.selector.matchLabels).toEqual(expectedSelector); - test('Expose uses the correct default values', () => { + // assert the deployment object has it. + expect(deployment.labelSelector).toEqual(expectedSelector); + +}); - const chart = Testing.chart(); +test('Can be exposed as via service', () => { - const deployment = new kplus.Deployment(chart, 'Deployment'); + const chart = Testing.chart(); - deployment.spec.podSpecTemplate.addContainer( + const deployment = new kplus.Deployment(chart, 'Deployment', { + containers: [ new kplus.Container({ image: 'image', port: 9300, }), - ); - - const service = deployment.expose(9200); - - const actual = service.spec._toKube(); + ], + }); - expect(actual.type).toEqual('ClusterIP'); - expect(actual.selector).toEqual({ - 'cdk8s.deployment': Names.toLabelValue(Node.of(deployment).path), - }); - expect(actual.ports![0].port).toEqual(9200); - expect(actual.ports![0].targetPort).toEqual(9300); + deployment.expose(9200, { serviceType: kplus.ServiceType.LOAD_BALANCER}); - }); + const spec = Testing.synth(chart)[1].spec; + expect(spec.type).toEqual('LoadBalancer'); + expect(spec.selector).toEqual({'cdk8s.deployment': 'test-Deployment-9e0110cd'}); + expect(spec.ports![0].port).toEqual(9200); + expect(spec.ports![0].targetPort).toEqual(9300); - test('Cannot be exposed if there are no containers in spec', () => { - const chart = Testing.chart(); +}); - const deployment = new kplus.Deployment(chart, 'Deployment'); +test('Expose uses the correct default values', () => { - expect(() => deployment.expose(9000)).toThrowError( - 'Cannot expose a deployment without containers', - ); - }); + const chart = Testing.chart(); - test('Generates spec lazily', () => { - const chart = Testing.chart(); - const deployment = new kplus.Deployment(chart, 'Deployment'); - deployment.spec.podSpecTemplate.addContainer( + const deployment = new kplus.Deployment(chart, 'Deployment', { + containers: [ new kplus.Container({ image: 'image', port: 9300, }), - ); + ], + }); - const actual = Testing.synth(chart)[0].spec.template.spec.containers[0]; + deployment.expose(9200); - // make sure the container exists in the spec even though it was added - // post instatiation. - expect(actual.image).toEqual('image'); - expect(actual.ports[0].containerPort).toEqual(9300); + const spec = Testing.synth(chart)[1].spec; + expect(spec.type).toEqual('ClusterIP'); - }); +}); + +test('Cannot be exposed if there are no containers in spec', () => { - test('Can be instantiated with an existing spec', () => { + const chart = Testing.chart(); - const spec = { - podSpecTemplate: { - containers: [new kplus.Container({ - image: 'image', - port: 9300, - })], - }, - }; + const deployment = new kplus.Deployment(chart, 'Deployment'); - const chart = Testing.chart(); - new kplus.Deployment(chart, 'Deployment', { - spec: spec, - }); + expect(() => deployment.expose(9000)).toThrowError( + 'Cannot expose a deployment without containers', + ); +}); - const actual = Testing.synth(chart)[0].spec.template.spec.containers[0]; +test('Synthesizes spec lazily', () => { - expect(actual.image).toEqual('image'); - expect(actual.ports[0].containerPort).toEqual(9300); + const chart = Testing.chart(); + + const deployment = new kplus.Deployment(chart, 'Deployment'); + deployment.addContainer( + new kplus.Container({ + image: 'image', + port: 9300, + }), + ); + + const container = Testing.synth(chart)[0].spec.template.spec.containers[0]; + + expect(container.image).toEqual('image'); + expect(container.ports[0].containerPort).toEqual(9300); - }); }); diff --git a/packages/cdk8s-plus/test/ingress.test.ts b/packages/cdk8s-plus/test/ingress.test.ts index 25309dd85c..809a1d2b1f 100644 --- a/packages/cdk8s-plus/test/ingress.test.ts +++ b/packages/cdk8s-plus/test/ingress.test.ts @@ -9,7 +9,7 @@ describe('IngressBackend', () => { const service = new Service(chart, 'my-service'); // WHEN - service.spec.serve(8899); + service.serve(8899); // THEN expect(IngressBackend.fromService(service)._toKube()).toEqual({ @@ -34,7 +34,7 @@ describe('IngressBackend', () => { const service = new Service(chart, 'my-service'); // WHEN - service.spec.serve(6011); + service.serve(6011); // THEN expect(() => IngressBackend.fromService(service, { port: 7766 })).toThrow(/backend defines port 7766 but service exposes port 6011/); @@ -46,7 +46,7 @@ describe('IngressBackend', () => { const service = new Service(chart, 'my-service'); // WHEN - service.spec.serve(6011); + service.serve(6011); // THEN expect(IngressBackend.fromService(service, { port: 6011 })._toKube()).toEqual({ @@ -54,16 +54,16 @@ describe('IngressBackend', () => { servicePort: 6011, }); }); - + test('service exposes multiple ports and the backend uses one of them', () => { // GIVEN const chart = Testing.chart(); const service = new Service(chart, 'my-service'); // WHEN - service.spec.serve(6011); - service.spec.serve(8899); - service.spec.serve(1011); + service.serve(6011); + service.serve(8899); + service.serve(1011); // THEN expect(IngressBackend.fromService(service, { port: 8899 })._toKube()).toEqual({ @@ -78,8 +78,8 @@ describe('IngressBackend', () => { const service = new Service(chart, 'my-service'); // WHEN - service.spec.serve(6011); - service.spec.serve(1111); + service.serve(6011); + service.serve(1111); // THEN expect(() => IngressBackend.fromService(service)).toThrow(/unable to determine service port since service exposes multiple ports/); @@ -91,8 +91,8 @@ describe('IngressBackend', () => { const service = new Service(chart, 'my-service'); // WHEN - service.spec.serve(6011); - service.spec.serve(1111); + service.serve(6011); + service.serve(1111); // THEN expect(() => IngressBackend.fromService(service, { port: 1234 })).toThrow(/service exposes ports 6011,1111 but backend is defined to use port 1234/); @@ -107,13 +107,13 @@ describe('Ingress', () => { test('defaultBackend property', () => { // GIVEN const chart = Testing.chart(); - const service = new Service(chart, 'my-service', { spec: { ports: [{port: 80}]} }); - + const service = new Service(chart, 'my-service', { ports: [{port: 80}]} ); + // WHEN new Ingress(chart, 'my-ingress', { defaultBackend: IngressBackend.fromService(service), }); - + // THEN expect(Testing.synth(chart).filter(x => x.kind === 'Ingress')).toStrictEqual([ { @@ -133,12 +133,12 @@ describe('Ingress', () => { test('addDefaultBackend()', () => { // GIVEN const chart = Testing.chart(); - const service = new Service(chart, 'my-service', { spec: { ports: [{port: 80}]} }); - + const service = new Service(chart, 'my-service', { ports: [{port: 80}]} ); + // WHEN const ingress = new Ingress(chart, 'my-ingress');; ingress.addDefaultBackend(IngressBackend.fromService(service)); - + // THEN expect(Testing.synth(chart).filter(x => x.kind === 'Ingress')).toStrictEqual([ { @@ -160,8 +160,8 @@ describe('Ingress', () => { test('addHostDefaultBackend()', () => { // GIVEN const chart = Testing.chart(); - const service = new Service(chart, 'my-service', { spec: { ports: [{port: 80}]} }); - + const service = new Service(chart, 'my-service', { ports: [{port: 80}]} ); + // WHEN const ingress = new Ingress(chart, 'my-ingress');; ingress.addHostDefaultBackend('my.host', IngressBackend.fromService(service)); @@ -181,7 +181,7 @@ describe('Ingress', () => { backend: { serviceName: 'test-my-service-pod-1c817a88', servicePort: 80, - }, + }, }, ], }, @@ -194,8 +194,8 @@ describe('Ingress', () => { test('addHostRule()', () => { // GIVEN const chart = Testing.chart(); - const service = new Service(chart, 'my-service', { spec: { ports: [{port: 80}]} }); - + const service = new Service(chart, 'my-service', { ports: [{port: 80}]} ); + // WHEN const ingress = new Ingress(chart, 'my-ingress');; ingress.addHostRule('my.host', '/foo', IngressBackend.fromService(service)); @@ -220,14 +220,14 @@ describe('Ingress', () => { backend: { serviceName: 'test-my-service-pod-1c817a88', servicePort: 80, - }, + }, }, { path: '/foo', backend: { serviceName: 'test-my-service-pod-1c817a88', servicePort: 80, - }, + }, }, ], }, @@ -240,14 +240,14 @@ describe('Ingress', () => { backend: { serviceName: 'test-my-service-pod-1c817a88', servicePort: 80, - }, + }, }, { path: '/', backend: { serviceName: 'test-my-service-pod-1c817a88', servicePort: 80, - }, + }, }, ], }, @@ -261,8 +261,8 @@ describe('Ingress', () => { test('addRule()', () => { // GIVEN const chart = Testing.chart(); - const service = new Service(chart, 'my-service', { spec: { ports: [{port: 80}]} }); - + const service = new Service(chart, 'my-service', { ports: [{port: 80}]} ); + // WHEN const ingress = new Ingress(chart, 'my-ingress'); ingress.addRule('/foo', IngressBackend.fromService(service)); @@ -284,14 +284,14 @@ describe('Ingress', () => { backend: { serviceName: 'test-my-service-pod-1c817a88', servicePort: 80, - }, + }, }, { path: '/foo/bar', backend: { serviceName: 'test-my-service-pod-1c817a88', servicePort: 80, - }, + }, }, ], }, @@ -306,8 +306,8 @@ describe('Ingress', () => { // GIVEN const chart = Testing.chart(); const service = new Service(chart, 'my-service'); - service.spec.serve(4000); - + service.serve(4000); + // WHEN new Ingress(chart, 'my-ingress', { rules: [ @@ -318,7 +318,7 @@ describe('Ingress', () => { { host: 'host.and', path: '/path/2', backend: IngressBackend.fromService(service) }, ], }); - + // THEN const expectedBackend = { serviceName: 'test-my-service-pod-1c817a88', servicePort: 4000 }; expect(Testing.synth(chart).filter(x => x.kind === 'Ingress')).toEqual([ @@ -333,7 +333,7 @@ describe('Ingress', () => { rules: [ { host: 'foo.bar', http: { paths: [ { backend: expectedBackend } ] } }, { http: { paths: [ { path: '/just/path', backend: expectedBackend } ] } }, - { host: 'host.and', http: { paths: [ + { host: 'host.and', http: { paths: [ { path: '/path', backend: expectedBackend }, { path: '/path/2', backend: expectedBackend }, ] } }, @@ -349,7 +349,7 @@ describe('Ingress', () => { // GIVEN const chart = Testing.chart(); const service = new Service(chart, 'my-service'); - service.spec.serve(4000); + service.serve(4000); // WHEN const ingress = new Ingress(chart, 'ingress', { @@ -365,7 +365,7 @@ describe('Ingress', () => { // GIVEN const chart = Testing.chart(); const service = new Service(chart, 'my-service'); - service.spec.serve(4000); + service.serve(4000); // THEN expect(() => new Ingress(chart, 'ingress', { @@ -378,7 +378,7 @@ describe('Ingress', () => { // GIVEN const chart = Testing.chart(); const service = new Service(chart, 'my-service'); - service.spec.serve(4000); + service.serve(4000); const ingress = new Ingress(chart, 'ingress'); // WHEN @@ -390,7 +390,7 @@ describe('Ingress', () => { // GIVEN const chart = Testing.chart(); const service = new Service(chart, 'my-service'); - service.spec.serve(4000); + service.serve(4000); const ingress = new Ingress(chart, 'ingress'); // WHEN @@ -403,11 +403,11 @@ describe('Ingress', () => { // GIVEN const chart = Testing.chart(); const service = new Service(chart, 'my-service'); - service.spec.serve(4000); + service.serve(4000); const ingress = new Ingress(chart, 'ingress'); // THEN expect(() => ingress.addRule('bad/path', IngressBackend.fromService(service))).toThrow(/ingress paths must begin with a "\/": bad\/path/); }); - + }); diff --git a/packages/cdk8s-plus/test/job.test.ts b/packages/cdk8s-plus/test/job.test.ts index 970a107aa9..c0f8dc6d5a 100644 --- a/packages/cdk8s-plus/test/job.test.ts +++ b/packages/cdk8s-plus/test/job.test.ts @@ -1,107 +1,57 @@ import * as kplus from '../src'; -import * as k from '../src/imports/k8s'; import { Testing } from 'cdk8s'; +import { RestartPolicy } from '../src'; -describe('JobSpecDefinition', () => { - test('Instantiation properties are all respected', () => { - const spec = new kplus.JobSpecDefinition({ - podSpecTemplate: { - containers: [ - new kplus.Container({ - image: 'image', - }), - ], - }, - ttlAfterFinished: kplus.Duration.seconds(1), - }); - - const actual: k.JobSpec = spec._toKube(); - - expect(actual.ttlSecondsAfterFinished).toEqual(1); - expect(actual.template.spec?.containers[0].image).toEqual('image'); +test('Applies default restart policy to pod spec', () => { + + const chart = Testing.chart(); + + const job = new kplus.Job(chart, 'Job', { + containers: [ new kplus.Container({ image: 'image' }) ], + ttlAfterFinished: kplus.Duration.seconds(1), }); - test('Does not modify existing restart policy of pod spec', () => { - const spec = new kplus.JobSpecDefinition({ - podSpecTemplate: { - containers: [ new kplus.Container({ image: 'image' }) ], - restartPolicy: kplus.RestartPolicy.ALWAYS, - }, - ttlAfterFinished: kplus.Duration.seconds(1), - }); + // assert the k8s spec has it. + const spec = Testing.synth(chart)[0].spec; + expect(spec.template.spec?.restartPolicy).toEqual('Never'); - const actual: k.JobSpec = spec._toKube(); + // assert the job object has it. + expect(job.restartPolicy).toEqual(RestartPolicy.NEVER); - expect(actual.template.spec?.restartPolicy).toEqual('Always'); - }); +}); - test('Applies default restart policy to pod spec', () => { - const spec = new kplus.JobSpecDefinition({ - podSpecTemplate: { - containers: [ new kplus.Container({ image: 'image' }) ], - }, - ttlAfterFinished: kplus.Duration.seconds(1), - }); +test('Does not modify existing restart policy of pod spec', () => { - const actual: k.JobSpec = spec._toKube(); + const chart = Testing.chart(); - expect(actual.template.spec?.restartPolicy).toEqual('Never'); + const job = new kplus.Job(chart, 'Job', { + containers: [ new kplus.Container({ image: 'image' }) ], + restartPolicy: RestartPolicy.ALWAYS, + ttlAfterFinished: kplus.Duration.seconds(1), }); + + // assert the k8s spec has it. + const spec = Testing.synth(chart)[0].spec; + expect(spec.template.spec?.restartPolicy).toEqual('Always'); + + // assert the job object has it. + expect(job.restartPolicy).toEqual(RestartPolicy.ALWAYS); + }); -describe('Job', () => { - test('Can provide existing spec', () => { - const chart = Testing.chart(); +test('Synthesizes spec lazily', () => { - const jobSpec: kplus.JobSpec = { - ttlAfterFinished: kplus.Duration.seconds(5), - }; + const chart = Testing.chart(); - const job = new kplus.Job(chart, 'Job', { - spec: jobSpec, - }); + const job = new kplus.Job(chart, 'Job'); - expect(job.spec.ttlAfterFinished?.toSeconds()).toEqual(5); - }); + job.addContainer( + new kplus.Container({ + image: 'image', + }), + ); + + const container = Testing.synth(chart)[0].spec.template.spec.containers[0]; + expect(container.image).toEqual('image'); - test('Generates spec lazily', () => { - const chart = Testing.chart(); - const job = new kplus.Job(chart, 'Job'); - - job.spec.podSpecTemplate.addContainer( - new kplus.Container({ - image: 'image', - }), - ); - - expect(Testing.synth(chart)).toMatchInlineSnapshot(` - Array [ - Object { - "apiVersion": "batch/v1", - "kind": "Job", - "metadata": Object { - "name": "test-job-default-e0180087", - }, - "spec": Object { - "template": Object { - "spec": Object { - "containers": Array [ - Object { - "env": Array [], - "image": "image", - "imagePullPolicy": "Always", - "name": "main", - "ports": Array [], - "volumeMounts": Array [], - }, - ], - "restartPolicy": "Never", - "volumes": Array [], - }, - }, - }, - }, - ] - `); - }); }); diff --git a/packages/cdk8s-plus/test/pod.test.ts b/packages/cdk8s-plus/test/pod.test.ts index 9dd56006d5..ebf77ed843 100644 --- a/packages/cdk8s-plus/test/pod.test.ts +++ b/packages/cdk8s-plus/test/pod.test.ts @@ -1,199 +1,81 @@ import * as kplus from '../src'; -import * as k8s from '../src/imports/k8s'; -import { RestartPolicy } from '../src'; import { Testing } from 'cdk8s'; -describe('PodSpecDefinition', () => { - test('Can add container post instantiation', () => { - const spec = new kplus.PodSpecDefinition(); +test('Can add container post instantiation', () => { - const container = new kplus.Container({ - image: 'image', - }); + const chart = Testing.chart(); - spec.addContainer(container); + const pod = new kplus.Pod(chart, 'Pod'); + pod.addContainer(new kplus.Container({ image: 'image' })); - const actual: k8s.Container[] = spec._toKube().containers; + const spec = Testing.synth(chart)[0].spec; - expect(actual[0].image).toEqual('image'); - }); + expect(spec.containers[0].image).toEqual('image'); - test('Must have at least one container', () => { - const spec = new kplus.PodSpecDefinition(); +}); - expect(() => spec._toKube()).toThrow( - 'PodSpec must have at least 1 container', - ); - }); +test('Must have at least one container', () => { - test('Can add volume post instantiation', () => { - const spec = new kplus.PodSpecDefinition(); + const chart = Testing.chart(); - const volume = kplus.Volume.fromEmptyDir('volume'); + new kplus.Pod(chart, 'Pod'); - // spec must have at least container - spec.addContainer( - new kplus.Container({ - image: 'image', - }), - ); + expect(() => Testing.synth(chart)).toThrow( + 'PodSpec must have at least 1 container', + ); - spec.addVolume(volume); +}); - const actual: k8s.Volume[] = spec._toKube().volumes!; +test('Can add volume post instantiation', () => { - expect(actual[0].name).toEqual('volume'); - expect(actual[0].emptyDir).toBeTruthy(); - }); + const chart = Testing.chart(); - test('Instantiation properties are all respected', () => { - const spec = new kplus.PodSpecDefinition({ - containers: [ - new kplus.Container({ - image: 'image', - name: 'container', - }), - ], - volumes: [kplus.Volume.fromEmptyDir('volume')], - restartPolicy: RestartPolicy.ALWAYS, - serviceAccount: kplus.ServiceAccount.fromServiceAccountName( - 'serviceAccount', - ), - }); - - const actual: k8s.PodSpec = spec._toKube(); - - const expected: k8s.PodSpec = { - containers: [ - { - name: 'container', - image: 'image', - imagePullPolicy: kplus.ImagePullPolicy.ALWAYS, - env: [], - command: undefined, - ports: [], - volumeMounts: [], - workingDir: undefined, - }, - ], - volumes: [ - { - name: 'volume', - emptyDir: { - medium: undefined, - sizeLimit: undefined, - }, - }, - ], - restartPolicy: 'Always', - serviceAccountName: 'serviceAccount', - }; - - expect(actual).toEqual(expected); + const pod = new kplus.Pod(chart, 'Pod', { + containers: [ + new kplus.Container({ image: 'image'}), + ], }); - test('Automatically adds volumes from container mounts', () => { - const spec = new kplus.PodSpecDefinition(); + const volume = kplus.Volume.fromEmptyDir('volume'); + pod.addVolume(volume); - const volume = kplus.Volume.fromEmptyDir('volume'); + const spec = Testing.synth(chart)[0].spec; - const container = new kplus.Container({ - image: 'image', - }); + expect(spec.volumes[0].name).toEqual('volume'); + expect(spec.volumes[0].emptyDir).toBeTruthy(); +}); - container.mount('/path/to/mount', volume); +test('Automatically adds volumes from container mounts', () => { - spec.addContainer(container); + const chart = Testing.chart(); - // make sure the volume configured in the mount exist on the pod spec. - const actual: k8s.Volume[] = spec._toKube().volumes!; + const pod = new kplus.Pod(chart, 'Pod'); - expect(actual[0].name).toEqual('volume'); - expect(actual[0].emptyDir).toBeTruthy(); - }); -}); + const volume = kplus.Volume.fromEmptyDir('volume'); -describe('Pod', () => { - test('Can instantiate without props', () => { - const chart = Testing.chart(); + const container = new kplus.Container({ image: 'image' }); + container.mount('/path/to/mount', volume); - const pod = new kplus.Pod(chart, 'Pod'); + pod.addContainer(container); - expect(pod.spec).toBeDefined(); - expect(pod.name).toBeDefined(); + const spec = Testing.synth(chart)[0].spec; - }); + expect(spec.volumes[0].name).toEqual('volume'); + expect(spec.volumes[0].emptyDir).toBeTruthy(); - test('Can instantiate with props', () => { - const spec = { - containers: [ - new kplus.Container({ - image: 'image', - }), - ], - }; +}); - const chart = Testing.chart(); +test('Synthesizes spec lazily', () => { - const pod = new kplus.Pod(chart, 'Pod', { - metadata: { name: 'name' }, - spec: spec, - }); + const chart = Testing.chart(); - expect(pod.spec.containers[0].image).toEqual('image'); - expect(pod.name).toEqual('name'); - }); + const pod = new kplus.Pod(chart, 'Pod', {}); + + const container = new kplus.Container({ image: 'image' }); + pod.addContainer(container); + + const spec = Testing.synth(chart)[0].spec; + + expect(spec.containers[0].image).toEqual('image'); - test('Generates spec lazily', () => { - const chart = Testing.chart(); - - const pod = new kplus.Pod(chart, 'Pod', {}); - - const volume = kplus.Volume.fromEmptyDir('volume'); - - const container = new kplus.Container({ - image: 'image', - }); - - container.mount('/path/to/mount', volume); - - pod.spec.addContainer(container); - - // if the spec was created during instantiation of the pod - // it would not have included the volume from the container. - expect(Testing.synth(chart)).toMatchInlineSnapshot(` - Array [ - Object { - "apiVersion": "v1", - "kind": "Pod", - "metadata": Object { - "name": "test-pod-cc5a4f6a", - }, - "spec": Object { - "containers": Array [ - Object { - "env": Array [], - "image": "image", - "imagePullPolicy": "Always", - "name": "main", - "ports": Array [], - "volumeMounts": Array [ - Object { - "mountPath": "/path/to/mount", - "name": "volume", - }, - ], - }, - ], - "volumes": Array [ - Object { - "emptyDir": Object {}, - "name": "volume", - }, - ], - }, - }, - ] - `); - }); }); diff --git a/packages/cdk8s-plus/test/service.test.ts b/packages/cdk8s-plus/test/service.test.ts index d022969117..216ff9c5f9 100644 --- a/packages/cdk8s-plus/test/service.test.ts +++ b/packages/cdk8s-plus/test/service.test.ts @@ -1,171 +1,129 @@ import * as kplus from '../src'; -import * as k from '../src/imports/k8s'; import { Testing } from 'cdk8s'; -describe('ServiceSpecDefinition', () => { - test('Instantiation properties are all accepted', () => { - const ports = [{ port: 9000, targetPort: 80, nodePort: 30080 }]; - const externalIPs = ['ExternalIP']; - const spec = new kplus.ServiceSpecDefinition({ - clusterIP: 'IP', - externalIPs: externalIPs, - ports: ports, - type: kplus.ServiceType.LOAD_BALANCER, - }); - - const actual: k.ServiceSpec = spec._toKube(); - - expect(actual.clusterIP).toEqual('IP'); - expect(actual.externalIPs).toEqual(externalIPs); - expect(actual.ports).toEqual(ports); - expect(actual.type).toEqual('LoadBalancer'); - }); +test('Must be configured with at least one port', () => { - test('Must be configured with at least one port', () => { - const spec = new kplus.ServiceSpecDefinition(); + const chart = Testing.chart(); - expect(() => spec._toKube()).toThrowError( - 'A service must be configured with a port', - ); - }); + new kplus.Service(chart, 'service'); + + expect(() => Testing.synth(chart)).toThrowError( + 'A service must be configured with a port', + ); - test('Can select by label', () => { - const spec = new kplus.ServiceSpecDefinition({ - ports: [{ port: 9000, targetPort: 80, nodePort: 30080 }], - }); +}); - spec.addSelector('key', 'value'); +test('Can select by label', () => { - const actual: k.ServiceSpec = spec._toKube(); + const chart = Testing.chart(); - expect(actual.selector).toEqual({ key: 'value' }); + const service = new kplus.Service(chart, 'service', { + ports: [{ port: 9000 }], }); - test('Can serve by port', () => { - const spec = new kplus.ServiceSpecDefinition(); + service.addSelector('key', 'value'); - spec.serve(9000, { targetPort: 80, nodePort: 30080 }); + // assert the k8s spec has it. + const spec = Testing.synth(chart)[0].spec; + expect(spec.selector).toEqual({ key: 'value' }); - const actual: k.ServiceSpec = spec._toKube(); + // assert the service object has it. + expect(service.selector).toEqual({ key: 'value' }); - expect(actual.ports).toEqual([ { port: 9000, targetPort: 80, nodePort: 30080 } ]); - }); +}); - test('AddDeployment() fails if the deployment does not have any containers', () => { - // GIVEN - const chart = Testing.chart(); - const service = new kplus.Service(chart, 'service'); - const dep = new kplus.Deployment(chart, 'dep'); +test('Can serve by port', () => { - // THEN - expect(() => service.addDeployment(dep, 1122)).toThrow(/Cannot expose a deployment without containers/); - }); + const chart = Testing.chart(); + + const service = new kplus.Service(chart, 'service'); + + service.serve(9000, { targetPort: 80, nodePort: 30080 }); + + // assert the k8s spec has it. + const spec = Testing.synth(chart)[0].spec; + expect(spec.ports).toEqual([ { port: 9000, targetPort: 80, nodePort: 30080 } ]); + + // assert the service object has it. + expect(service.ports).toEqual([ { port: 9000, targetPort: 80, nodePort: 30080 } ]); }); -describe('Service', () => { - test('Can accept an existing spec', () => { - const chart = Testing.chart(); - const spec: kplus.ServiceSpec = { - clusterIP: 'cluster-ip', - } - const service = new kplus.Service(chart, 'Service', { - spec: spec, - }); - - expect(service.spec.clusterIP).toEqual('cluster-ip'); - }); +test('Cannot add a deployment if the deployment does not have any containers', () => { - test('Generates spec lazily', () => { - const chart = Testing.chart(); - const service = new kplus.Service(chart, 'Service'); - - service.spec.addSelector('key', 'value'); - service.spec.serve(9000); - - expect(Testing.synth(chart)).toMatchInlineSnapshot(` - Array [ - Object { - "apiVersion": "v1", - "kind": "Service", - "metadata": Object { - "name": "test-service-pod-9164a1e2", - }, - "spec": Object { - "externalIPs": Array [], - "ports": Array [ - Object { - "port": 9000, - }, - ], - "selector": Object { - "key": "value", - }, - "type": "ClusterIP", - }, - }, - ] - `); - }); + const chart = Testing.chart(); - test('AddDeployment() can be used to associate a deployment with a service', () => { - // GIVEN - const chart = Testing.chart(); - const service = new kplus.Service(chart, 'service'); - const dep = new kplus.Deployment(chart, 'dep'); - dep.spec.podSpecTemplate.addContainer(new kplus.Container({ image: 'foo', port: 7777 })); - - // WHEN - service.addDeployment(dep, 1122); - - // THEN - const expectedSelector = {'cdk8s.deployment': 'test-dep-b18049c6'}; - expect(dep.spec._toKube().selector.matchLabels).toEqual(expectedSelector); - expect(dep.spec._toKube().template.metadata?.labels).toEqual(expectedSelector); - expect(service.spec._toKube()).toEqual({ - clusterIP: undefined, - externalIPs: [], - ports: [{ - nodePort: undefined, - port: 1122, - targetPort: 7777, - }], - selector: expectedSelector, - type: 'ClusterIP', - }); - }); - - test('addDeployment() fails if the deployment dose not have a label selector', () => { - // GIVEN - const chart = Testing.chart(); - const service = new kplus.Service(chart, 'service'); - const dep = new kplus.Deployment(chart, 'dep', { - defaultSelector: false, - spec: { - podSpecTemplate: { - containers: [ new kplus.Container({ image: 'foo' }) ], - }, - }, - }); - - // THEN - expect(() => service.addDeployment(dep, 1122)).toThrow(/deployment does not have a label selector/); + const service = new kplus.Service(chart, 'service'); + const deployment = new kplus.Deployment(chart, 'dep'); + + // THEN + expect(() => service.addDeployment(deployment, 1122)) + .toThrow(/Cannot expose a deployment without containers/); + +}); + +test('Synthesizes spec lazily', () => { + + const chart = Testing.chart(); + + const service = new kplus.Service(chart, 'Service'); + + service.addSelector('key', 'value'); + service.serve(9000); + + const spec = Testing.synth(chart)[0].spec; + expect(spec.selector).toEqual({key: 'value'}); + expect(spec.ports).toEqual([{ port: 9000 }]); + +}); + +test('Can associate a deployment with an existing service', () => { + + const chart = Testing.chart(); + + const service = new kplus.Service(chart, 'service'); + const deployment = new kplus.Deployment(chart, 'dep'); + deployment.addContainer(new kplus.Container({ image: 'foo', port: 7777 })); + + service.addDeployment(deployment, 1122); + + const expectedSelector = {'cdk8s.deployment': 'test-dep-b18049c6'}; + + const deploymentSpec = Testing.synth(chart)[1].spec; + const serviceSpec = Testing.synth(chart)[0].spec; + expect(deploymentSpec.selector.matchLabels).toEqual(expectedSelector); + expect(deploymentSpec.template.metadata?.labels).toEqual(expectedSelector); + expect(serviceSpec.selector).toEqual(expectedSelector); + +}); + +test('Cannot add a deployment if it does not have a label selector', () => { + + const chart = Testing.chart(); + + const service = new kplus.Service(chart, 'service'); + const deployment = new kplus.Deployment(chart, 'dep', { + defaultSelector: false, + containers: [ new kplus.Container({ image: 'foo' }) ], }); - test('addDeployment() fails if a selector is already defined for this service', () => { - // GIVEN - const chart = Testing.chart(); - const service = new kplus.Service(chart, 'service'); - const dep1 = new kplus.Deployment(chart, 'dep1', { - spec: { - podSpecTemplate: { - containers: [ new kplus.Container({ image: 'foo' }) ], - }, - }, - }); - service.spec.addSelector('random', 'selector'); - - // THEN - expect(() => service.addDeployment(dep1, 1010)).toThrow(/a selector is already defined for this service. cannot add a deployment/); + expect(() => service.addDeployment(deployment, 1122)) + .toThrow(/deployment does not have a label selector/); + +}); + +test('Cannot add a deployment if a selector is already defined for this service', () => { + + const chart = Testing.chart(); + const service = new kplus.Service(chart, 'service'); + + const deployment = new kplus.Deployment(chart, 'dep1', { + containers: [ new kplus.Container({ image: 'foo' }) ], }); + service.addSelector('random', 'selector'); + + // THEN + expect(() => service.addDeployment(deployment, 1010)) + .toThrow(/a selector is already defined for this service. cannot add a deployment/); + });