Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plus-17): add StatefulSet construct #400

Merged
merged 9 commits into from Jan 21, 2021

Conversation

abierbaum
Copy link
Contributor

This is a first pass at a statefulset construct. It works well for the cases I need and should serve well as a starting point. Note that I copied the concepts from Deployment so it should look pretty familiar.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license

@abierbaum abierbaum changed the title feat(plus): add StatefulSet construct feat(plus-17): add StatefulSet construct Nov 19, 2020
@abierbaum
Copy link
Contributor Author

@iliapolo Any thoughts on this one? I know there could probably be a set of higher level constructs still for some common statefulset cases, but this seems like a pretty good place to start as it basically mirrors Deployment.

@iliapolo
Copy link
Member

iliapolo commented Nov 29, 2020

@abierbaum only glanced through it, looks ok, i'll take a closer look soon.

Thanks!

*
* @default undefined - allocate a default service based upon servicePort and serviceName.
*/
readonly service?: ServiceProps;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can just accept a Service here instead, it will prevent some logic duplication and keep things more modular. Its ok to require the caller to instantiate other constructs.

Copy link
Contributor Author

@abierbaum abierbaum Dec 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought about that but it ends up making the initialization more complex. I was trying to get an API as easy to use as the deployment expose() call.

For example when I am using this in my production chart I end up calling as below. (note: I could have left off the service props in this case since this is default, but I put it there to be explicit for other people that come along later). The key is that people can just say the service name and port and get a good statefulset with standard options, but they can override the type if they want to do something like a node port or something like that.

const redis_set = new kplus.StatefulSet(this, 'redis-set', {
         serviceName: 'redis-srv',
         replicas: 2,
         containers: [redis_container],
         servicePort: redis_port,
         service: {
            type: kplus.ServiceType.CLUSTER_IP,
            clusterIP: undefined, // force it to allocate
         },
      });

If the user had to allocate the Service it would look like:

const redis_srv = new Service(this, 'RedisService', {
   metadata: {name: 'redis-srv'},
   type: ServiceType.CLUSTER_IP,
   clusterIP: undefined,
   ports: [{port: redis_port}],
})

const redis_set = new kplus.StatefulSet(this, 'redis-set', {
         serviceName: 'redis-srv',
         replicas: 2,
         containers: [redis_container],
         servicePort: redis_port,
         service: redis_srv
      });

And the callee would have to make sure the service name strings match and the ports match. Seems like this is the type of thing we would want to wrap to make it simpler to handle and then if they need to modify the service they can patch it.

What do you think though? I probably missed something in this one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abierbaum

I thought about that but it ends up making the initialization more complex. I was trying to get an API as easy to use as the deployment expose() call.

This is a little different, because a stateful set cannot exist without a service, as opposed to a deployment. I understand the idea was to implicitly create a service, mimicking the expose behavior, but this should be implemented by accepting an optional Service, and defaulting to auto creating it using the information of the stateful set (i.e assigning some name based on the stateful set name, and looking up the correct ports using the containers of the stateful set).

We can definitely do something like that in the future, but I think it's better to think about that in a subsequent PR.

And the callee would have to make sure the service name strings match and the ports match.

Why? what I was thinking the caller would have to do is this:

const redis_set = new kplus.StatefulSet(this, 'redis-set', {
         replicas: 2,
         containers: [redis_container],
         service: redis_srv
      });

Notice that serviceName and servicePort is not passed, because they can be extracted from the service object using service.name and service.ports. Which means we don't delegate any burden on the caller except for calling new Service.

Will that not work?

*
* @default undefined
*/
readonly servicePort?: number;
Copy link
Member

@iliapolo iliapolo Dec 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Won't be needed if we accept a Service.

*
* @default undefined
*/
readonly serviceName?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We would still need it if the Service was optional. But if the service was required we could remove it yes.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, my plan was to make it required at the moment.

packages/cdk8s-plus-17/src/statefulset.ts Outdated Show resolved Hide resolved
*
* Returns a a copy. Use `selectByLabel()` to add labels.
*/
get labelSelector(): Record<string, string> {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be consistent:

Suggested change
get labelSelector(): Record<string, string> {
public get labelSelector(): Record<string, string> {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Got it. I am so used to my production projects just adding this type of thing automatically using prettier and tslint that I forgot. Sorry.

@github-actions
Copy link
Contributor

github-actions bot commented Dec 7, 2020

This PR has not received a response in a while. If you want to keep this issue open, please leave a comment below and auto-close will be canceled.

@iliapolo
Copy link
Member

iliapolo commented Dec 7, 2020

Dont close

@abierbaum
Copy link
Contributor Author

@iliapolo Sorry for the delay. I got this working to where I was able to use it in production and forgot to come back to clean it up for upstream consumption. I have reviewed your comments, made some fixes, and added some feedback. Please let me know your thoughts. I would love to get this through this week.

*
* @default undefined - allocate a default service based upon servicePort and serviceName.
*/
readonly service?: ServiceProps;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abierbaum

I thought about that but it ends up making the initialization more complex. I was trying to get an API as easy to use as the deployment expose() call.

This is a little different, because a stateful set cannot exist without a service, as opposed to a deployment. I understand the idea was to implicitly create a service, mimicking the expose behavior, but this should be implemented by accepting an optional Service, and defaulting to auto creating it using the information of the stateful set (i.e assigning some name based on the stateful set name, and looking up the correct ports using the containers of the stateful set).

We can definitely do something like that in the future, but I think it's better to think about that in a subsequent PR.

And the callee would have to make sure the service name strings match and the ports match.

Why? what I was thinking the caller would have to do is this:

const redis_set = new kplus.StatefulSet(this, 'redis-set', {
         replicas: 2,
         containers: [redis_container],
         service: redis_srv
      });

Notice that serviceName and servicePort is not passed, because they can be extracted from the service object using service.name and service.ports. Which means we don't delegate any burden on the caller except for calling new Service.

Will that not work?

*
* @default undefined
*/
readonly serviceName?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, my plan was to make it required at the moment.

throw new Error('Cannot build a stateful set with a Service without ports.');
}

this._service = new Service(this, 'Service', {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also makes it impossible to accept an existing service in the future. Since a stateful set only needs a service name, I imagine that eventually it should accept an IService, which we don't currently have.

});

if (!props.servicePort && !props.service?.ports) {
throw new Error('Cannot build a stateful set with a Service without ports.');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will move to the _toKube method and validate the ports property of the given service object.

@github-actions
Copy link
Contributor

This PR has not received a response in a while. If you want to keep this issue open, please leave a comment below and auto-close will be canceled.

@iliapolo
Copy link
Member

Dont close

@abierbaum
Copy link
Contributor Author

abierbaum commented Dec 31, 2020

@iliapolo I think this last update should address the core issue pretty well. I changed it to take a Service as input. After seeing it in action, I agree this is a good change for this use case.

note: I rebased off latest master.

abierbaum and others added 7 commits December 31, 2020 10:00
This may have been a good idea, but it makes things painful
when trying to apply changes for statefulset's where you want
to have an assigned ClusterIP automatically.  You end up with
immutable change warnings because you really don't want to pass '',
you want to pass undefined so you just get whatever it assigned.
Co-authored-by: Eli Polonsky <Eli.polonsky@gmail.com>
@abierbaum
Copy link
Contributor Author

@iliapolo Have you had a chance to get back to this one yet?

Copy link
Member

@iliapolo iliapolo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@abierbaum this is great. Just some minor nit-picks around naming conventions :)

import { Volume } from './volume';


export enum PodManagementPolicy {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add some doc strings. Should also be available in the k8s spec.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated.


const chart = Testing.chart();

const service = new kplus.Service(chart, 'test_service', {ports: [{port: 80}] });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const service = new kplus.Service(chart, 'test_service', {ports: [{port: 80}] });
const service = new kplus.Service(chart, 'TestService', {ports: [{port: 80}] });

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed.

Comment on lines 104 to 106
expect(srv_spec.type).toEqual(kplus.ServiceType.CLUSTER_IP);
expect(srv_spec.clusterIP).toBeUndefined();
expect(srv_spec.ports![0].port).toEqual(80);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't seem related to a statefulset test..if its not covered in the service tests, lets add it there, but if it is, we can drop it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed.


const resources = Testing.synth(chart);
const srv_spec = resources[0].spec;
const set_spec = resources[1].spec;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const set_spec = resources[1].spec;
const setSpec = resources[1].spec;

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

changed.

Comment on lines 133 to 134
expect(srv_spec.type).toEqual(kplus.ServiceType.NODE_PORT);
expect(srv_spec.ports![0].port).toEqual(9000);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

again, doesn't seem necessary.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed.

@abierbaum
Copy link
Contributor Author

@iliapolo I have made the changes suggested. I apologize to all for the amount time it took to respond and get everything completed.

iliapolo
iliapolo previously approved these changes Jan 21, 2021
@mergify
Copy link
Contributor

mergify bot commented Jan 21, 2021

Your pull request will be updated and merged automatically (do not update manually).

@mergify mergify bot dismissed iliapolo’s stale review January 21, 2021 18:06

Pull request has been modified.

@iliapolo
Copy link
Member

@abierbaum No worries, i've been lagging as well :)

Thanks so much for this PR, its awesome.

@mergify
Copy link
Contributor

mergify bot commented Jan 21, 2021

Your pull request will be updated and merged automatically (do not update manually).

@mergify mergify bot merged commit 98aad99 into cdk8s-team:master Jan 21, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants