Skip to main content

Component Options

In the previous section, we moved all the generation code under the generateResources function of our new component. This code includes the constants that are used to generate the resources, such as name, namespace, and image.

const name = "example-app";
const namespace = "default";
const image = "nginx";
const replicas = 1;

But, the users of our package may want to change these values. For example, they may want to use a different image or a different number of replicas. To allow this, we need to add options to our component. First, let's create a new file and define our Options class:

options.ts
export class Options {
name?: string;
namespace?: string;
image?: string;
replicas?: number;
}

Note that, in TypeScript declaration, all the options are optional (they have ? suffixes). This means that the user does not have to provide values for these options, therefore we should provide default values for them in our component.

Now, we need to take the options from the user in the constructor of the component. To do this, we will modify the component class to accept an instance of Options as a parameter.

component.ts
import * as anemos from "@ohayocorp/anemos";
import { Options } from "./options";

export class Component extends anemos.Component {
options: Options;

constructor(options?: Options) {
super();

this.options = options ?? {};

this.addAction(anemos.steps.generateResources, this.generateResources);
}

generateResources = (context: anemos.BuildContext) => {
const name = "example-app";
const namespace = "default";
const image = "nginx";
const replicas = 1;

context.addDocument(
`deployment.yaml`,
`
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${name}
namespace: ${namespace}
spec:
replicas: ${replicas}
selector:
matchLabels:
app: ${name}
template:
metadata:
labels:
app: ${name}
spec:
containers:
- name: app
image: ${image}
ports:
- containerPort: 80
`);

context.addDocument(
`service.yaml`,
{
apiVersion: "v1",
kind: "Service",
metadata: {
name: name,
namespace: namespace,
},
spec: {
selector: {
app: name
},
ports: [
{
protocol: "TCP",
port: 80,
targetPort: 80
}
]
}
});
};
}

Options itself is also optional, so we assign an empty object to this.options if the user does not provide anything.

Now, we can access the options in the generateResources function using this.options. However, we need a way to ensure that default values are used if the user doesn't provide specific options. Anemos provides a dedicated step for this purpose called sanitize. This step runs very early, making it the ideal place to initialize options before they are used in later steps like generateResources.

Let's update the component class to add an action to be run during the sanitize step. Then use the options in the generateResources step instead of the constants we defined earlier.

component.ts
import * as anemos from "@ohayocorp/anemos";
import { Options } from "./options";

export class Component extends anemos.Component {
options: Options;

constructor(options?: Options) {
super();

this.options = options ?? {};

this.addAction(anemos.steps.sanitize, this.sanitize);
this.addAction(anemos.steps.generateResources, this.generateResources);
}

sanitize = () => {
// Assign default values to the options if they are not provided.
const options = this.options;

options.name ??= "example-app";
options.namespace ??= "default";
options.image ??= "nginx";
options.replicas ??= 1;
};

generateResources = (context: anemos.BuildContext) => {
const name = this.options.name;
const namespace = this.options.namespace;
const image = this.options.image;
const replicas = this.options.replicas;

context.addDocument(
`deployment.yaml`,
`
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${name}
namespace: ${namespace}
spec:
replicas: ${replicas}
selector:
matchLabels:
app: ${name}
template:
metadata:
labels:
app: ${name}
spec:
containers:
- name: app
image: ${image}
ports:
- containerPort: 80
`);

context.addDocument(
`service.yaml`,
{
apiVersion: "v1",
kind: "Service",
metadata: {
name: name,
namespace: namespace,
},
spec: {
selector: {
app: name
},
ports: [
{
protocol: "TCP",
port: 80,
targetPort: 80
}
]
}
});
};
}

In the sanitize function, we use the nullish coalescing assignment operator (??=) to set the default value for each option only if it hasn't already been provided by the user.

With the options and default values handled, we can now instantiate the component and pass specific options. For example, to set the image to nginx:1.27 and the replicas to 3, while keeping the default values for name and namespace, we would modify our main script as follows:

index.ts
import * as anemos from "@ohayocorp/anemos";
import { Component } from "./component";

const builder = new anemos.Builder("1.31", anemos.KubernetesDistribution.Minikube, anemos.EnvironmentType.Development);

builder.addComponent(new Component({
image: "nginx:1.27",
replicas: 3,
}));

builder.build();

Now, when you build the application, replicas will be set to 3, the image will be nginx:1.27, and the name and namespace will remain as example-app and default, respectively.

anemos build --tsc . dist/index.js

Diff of the deployment.yaml file:

deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: example-app
namespace: default
spec:
- replicas: 1
+ replicas: 3
selector:
matchLabels:
app: example-app
template:
metadata:
labels:
app: example-app
spec:
containers:
- name: app
- image: nginx
+ image: nginx:1.27
ports:
- containerPort: 80