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:
- TypeScript
- JavaScript
export class Options {
name?: string;
namespace?: string;
image?: string;
replicas?: number;
}
class Options {
name;
namespace;
image;
replicas;
}
module.exports = Options;
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.
- TypeScript
- JavaScript
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
}
]
}
});
};
}
const anemos = require("@ohayocorp/anemos");
class Component extends anemos.Component {
constructor(options) {
super();
this.options = options ?? {};
this.addAction(anemos.steps.generateResources, this.generateResources);
}
generateResources = (context) => {
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
}
]
}
});
};
}
module.exports = Component;
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.
- TypeScript
- JavaScript
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
}
]
}
});
};
}
const anemos = require("@ohayocorp/anemos");
class Component extends anemos.Component {
constructor(options) {
super();
this.options = options ?? {};
this.addAction(anemos.steps.sanitize, this.sanitize);
this.addAction(anemos.steps.generateResources, this.generateResources);
}
sanitize = (context) => {
// 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) => {
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
}
]
}
});
};
}
module.exports = Component;
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:
- TypeScript
- JavaScript
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();
const anemos = require("@ohayocorp/anemos");
const Component = require("./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.
- TypeScript
- JavaScript
anemos build --tsc . dist/index.js
anemos build index.js
Diff of the deployment.yaml
file:
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