| title | Quarkus Operator Tutorial |
|---|---|
| linkTitle | Tutorial |
| weight | 30 |
| description | An in-depth walkthough of building and running a Quarkus-based operator. |
- Java through the installation guide.
- User authorized with
cluster-adminpermissions. - Maven installation installation guide
We will create a sample project to let you know how it works and this sample will:
- Create a Memcached Deployment if it doesn't exist
- Ensure that the Deployment size is the same as specified by the Memcached Custom Resource spec
- Update the Memcached Custom Resource status using the status writer with the names of the Custom Resource's pods
Use the CLI to create a new memcached-quarkus-operator project:
mkdir memcached-quarkus-operator
cd memcached-quarkus-operator
# we'll use a domain of example.com
# so all API groups will be <group>.example.com
operator-sdk init --plugins quarkus --domain example.com --project-name memcached-quarkus-operatoroperator-sdk init generates pom.xml file. This file contains all the dependencies required to run the operator.
The main program of the operator MemcachedQuarkusOperator.java initializes and runs the operator. The operator uses java-operator-sdk which is similar to the Go lang version of controller-runtime.
The code below will initialize and define the informers/watches for your operator.
@Override
public int run(String... args) throws Exception {
operator.start();
Quarkus.waitForExit();
return 0;
}
Create a new Custom Resource Definition (CRD) API with group cache version v1 and Kind Memcached. The plugin, still in its alpha state, will output debug messages which are normal.
create api command will scaffold the MemcachedController, MemcachedSpec, MemcachedStatus and Memcached.
$ operator-sdk create api --plugins quarkus --group cache --version v1 --kind Memcached
InjectResource called
UpdateResource called
Scaffold called
NewCreateAPIScaffolder called
InjectFS called
api.Scaffold()
PostScaffold called
...After the create api command the file structure will be shown as below.
$ tree
.
├── pom.xml
├── PROJECT
└── src
└── main
├── java
│ └── com
│ └── example
│ ├── MemcachedController.java
│ ├── Memcached.java
│ ├── MemcachedQuarkusOperator.java
│ ├── MemcachedSpec.java
│ └── MemcachedStatus.java
└── resources
└── application.properties
6 directories, 8 files
For an in-depth explanation of Kubernetes APIs and the group-version-kind model, check out these kubebuilder docs.
The java-operator-plugins project uses the APIs from java-operator-sdk. The java-operator-sdk library is to Java what controller-runtime is to Go. It provides a framework to help Java projects interact with the Kubernetes API.
Initially, the scaffolded Spec file will be empty. The operator developer needs to add attributes to this file according to his need. For the Memcached example, we added the size field as shown below example.
// MemcachedSpec defines the desired state of Memcached
public class MemcachedSpec {
// Add Spec information here
// Size is the size of the memcached deployment
private Integer size;
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
}
Similar to the Spec file MemcachedStatus file got scaffolded as part of the create api command. The user has to modify a Status file. For the Memcached example, we too list of nodes as shown below.
The nodes field is a list of string values and it contains the name of the Memcached pods.
import java.util.ArrayList;
import java.util.List;
// MemcachedStatus defines the observed state of Memcached
public class MemcachedStatus {
// Add Status information here
// Nodes are the names of the memcached pods
private List<String> nodes;
public List<String> getNodes() {
if (nodes == null) {
nodes = new ArrayList<>();
}
return nodes;
}
public void setNodes(List<String> nodes) {
this.nodes = nodes;
}
}
Note The Node field is just to illustrate an example of a Status field. In real cases, it would be recommended to use Conditions.
Memcached file scaffolded via create api command. It extends the properties of MemcachedSpec and MemcachedStatus.
// Memcached is the Schema for the memcacheds API
@Version("v1alpha1")
@Group("cache.example.com")
public class Memcached extends CustomResource<MemcachedSpec, MemcachedStatus>
implements Namespaced {}
Create a file with the name crd.yaml. CRD enables users to add their own/custom objects to the Kubernetes cluster.
This file will contain the below content.
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.4.1
name: memcacheds.cache.example.com
spec:
group: cache.example.com
names:
kind: Memcached
listKind: MemcachedList
plural: memcacheds
singular: memcached
scope: Namespaced
versions:
- name: v1
schema:
openAPIV3Schema:
...
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
Create the CRD:
kubectl apply -f crd.yaml
Create the sample Memcached Custom Resource manifest at k8s/samples/memcached-sample.yaml and define the spec as the following.
This file will contain the below content.
apiVersion: cache.example.com/v1
kind: Memcached
metadata:
name: memcached-sample
spec:
# Add fields here
size: 1
Create the Custom Resource:
kubectl apply -f memcached-sample.yaml
Add the below-mentioned code snippet in MemcachedController.java file. Initially, this file will contain the empty methods createOrUpdateResource and deleteResource. Please add the below code in respective methods as part of controller logic. Also, add the createMemcachedDeployment method that will create the Deployment for your operator.
Note: Next two subsections explain the two methods createOrUpdateResource and deleteResource. These two methods get called whenever some update/create/delete event occurs in the cluster.
These methods are already scaffolded as part of the create api command. In this memcached example, we will need to watch the deployment so we can react to size changes. We will accomplish this in the steps below.
First, let's get the Deployment. In the MemachedController.java, find the createOrUpdateResource method and add the following code below the // TODO: fill in logic comment. It should look like:
Deployment deployment =
client
.apps()
.deployments()
.inNamespace(resource.getMetadata().getNamespace())
.withName(resource.getMetadata().getName())
.get();
If the deployment is null that means we need to create the deployment for it. In the MemachedController.java, find the createOrUpdateResource method and add the following code.
Below code will verify that Deployment within the cluster got created or not. If deployment is null then it will create deployment. createMemcachedDeployment(resource) creates the Deployment and then it will be applied by using client.apps().deployments().create(newDeployment); code. createMemcachedDeployment(resource) method explained in the next part.
if (deployment == null) {
Deployment newDeployment = createMemcachedDeployment(resource);
client.apps().deployments().create(newDeployment);
return UpdateControl.noUpdate();
}
Once we create the deployment, we need to decide whether we have to reconcile it or not.
If there is no need of reconciliation then return UpdateControl.noUpdate() else we need to return UpdateControl.updateStatusSubResource(resource)
After the Creation of the Deployment, get the current and required replicas by using the below code.
int currentReplicas = deployment.getSpec().getReplicas();
int requiredReplicas = resource.getSpec().getSize();
If currentReplicas does not match with requiredReplicas then we need to update the Deployment. This process will be done by using the below code.
if (currentReplicas != requiredReplicas) {
deployment.getSpec().setReplicas(requiredReplicas);
client.apps().deployments().createOrReplace(deployment);
return UpdateControl.noUpdate();
}
Then, let's get the list of pods and pod names. In the MemachedController.java, find the createOrUpdateResource method and add the following code. It should look like:
List<Pod> pods =
client
.pods()
.inNamespace(resource.getMetadata().getNamespace())
.withLabels(labelsForMemcached(resource))
.list()
.getItems();
List<String> podNames =
pods.stream().map(p -> p.getMetadata().getName()).collect(Collectors.toList());
Now, check whether resources get created or not. Then, it verifies podnames with the Memcached resources. If there is a mismatch in either of these conditions then we need to do a reconciliation.
if (resource.getStatus() == null
|| !CollectionUtils.isEqualCollection(podNames, resource.getStatus().getNodes())) {
if (resource.getStatus() == null) resource.setStatus(new MemcachedStatus());
resource.getStatus().setNodes(podNames);
return UpdateControl.updateStatusSubResource(resource);
}
The complete createOrUpdateResource function will look like below in MemachedController.java.
@Override
public UpdateControl<Memcached> createOrUpdateResource(
Memcached resource, Context<Memcached> context) {
// TODO: fill in logic
System.out.println("Create or Update Control");
Deployment deployment =
client
.apps()
.deployments()
.inNamespace(resource.getMetadata().getNamespace())
.withName(resource.getMetadata().getName())
.get();
System.out.println(deployment);
if (deployment == null) {
Deployment newDeployment = createMemcachedDeployment(resource);
client.apps().deployments().create(newDeployment);
return UpdateControl.noUpdate();
}
int currentReplicas = deployment.getSpec().getReplicas();
int requiredReplicas = resource.getSpec().getSize();
if (currentReplicas != requiredReplicas) {
deployment.getSpec().setReplicas(requiredReplicas);
client.apps().deployments().createOrReplace(deployment);
return UpdateControl.noUpdate();
}
List<Pod> pods =
client
.pods()
.inNamespace(resource.getMetadata().getNamespace())
.withLabels(labelsForMemcached(resource))
.list()
.getItems();
List<String> podNames =
pods.stream().map(p -> p.getMetadata().getName()).collect(Collectors.toList());
if (resource.getStatus() == null
|| !CollectionUtils.isEqualCollection(podNames, resource.getStatus().getNodes())) {
if (resource.getStatus() == null) resource.setStatus(new MemcachedStatus());
resource.getStatus().setNodes(podNames);
return UpdateControl.updateStatusSubResource(resource);
}
return UpdateControl.noUpdate();
}
Create createMemcachedDeployment method and add the below code snippet. This method simply creates the Deployment.
private Deployment createMemcachedDeployment(Memcached m) {
return new DeploymentBuilder()
.withMetadata(
new ObjectMetaBuilder()
.withName(m.getMetadata().getName())
.withNamespace(m.getMetadata().getNamespace())
.withOwnerReferences(
new OwnerReferenceBuilder()
.withApiVersion("v1alpha1")
.withKind("Memcached")
.withName(m.getMetadata().getName())
.withUid(m.getMetadata().getUid())
.build())
.build())
.withSpec(
new DeploymentSpecBuilder()
.withReplicas(m.getSpec().getSize())
.withSelector(
new LabelSelectorBuilder().withMatchLabels(labelsForMemcached(m)).build())
.withTemplate(
new PodTemplateSpecBuilder()
.withMetadata(
new ObjectMetaBuilder().withLabels(labelsForMemcached(m)).build())
.withSpec(
new PodSpecBuilder()
.withContainers(
new ContainerBuilder()
.withImage("memcached:1.4.36-alpine")
.withName("memcached")
.withCommand("memcached", "-m=64", "-o", "modern", "-v")
.withPorts(
new ContainerPortBuilder()
.withContainerPort(11211)
.withName("memcached")
.build())
.build())
.build())
.build())
.build())
.build();
}
The deleteResource method is an implemented method. The deletion part will be taken care of by the Java Operator SDK library, that's why it is empty.
The code snippet for deleteResource is as shown below.
@Override
public DeleteControl deleteResource(Memcached resource, Context<Memcached> context) {
// nothing to do here...
// framework takes care of deleting the resource object
// k8s takes care of deleting deployment and pods because of ownerreference set
System.out.println("Delete Control");
return DeleteControl.DEFAULT_DELETE;
}
The following steps will show how to run your operator locally.
Compile your operator with the below command
mvn clean install
It will create a .jar file for your operator in target/quarkus-app. Now, run the jar file using the below command.
java -jar quarkus-run.jar
This command will run your operator locally. You can check cluster pods and deployment with the below commands.
kubectl get deployment
kubectl get pods
Delete One of the Pod forcefully then Memcached operator will create new automatically.
kubectl delete pod pod-name
In the end, change the size from the memcached-sample.yaml file and apply it to the cluster. After these steps, an operator will make sure that the cluster has an updated number of pods in it.