Over the last few posts, I have been building a small Greeting Kubernetes operator.
It started with a simple CRD:
Greeting
|
v
Controller
|
v
ConfigMap
That small example helped me understand a lot of Kubernetes operator concepts:
Custom Resources
Controllers
Reconciliation
Dependent Resources
Status
Conditions
RBAC
Failures
Retries
Events
Manual Reconciliation
Owner References
Garbage Collection
At this point, I felt that continuing to add more concepts to the Greeting operator would mostly make the example more complicated.
So I decided to keep it as a learning/reference operator and start working on the actual idea behind Platform Lab.
The first real platform API is:
Application
What I Want the Platform to Do
The original idea behind Platform Lab is simple.
As an application developer, I do not want to write all of this every time:
Deployment
Service
ConfigMap
Secret
ServiceAccount
NetworkPolicy
AuthorizationPolicy
Gateway configuration
Observability configuration
Instead, I want the developer-facing API to be much smaller.
Something like:
apiVersion: platform.shubforge.dev/v1alpha1
kind: Application
metadata:
name: greeting-service
spec:
image: greeting-service:1.0.0
replicas: 1
port:
containerPort: 8080
Eventually, the platform operator should translate that into:
Application
|
v
Platform Operator
|
+---- Deployment
|
+---- Service
And later, the same platform API can grow into other capabilities.
Application
|
+---- Deployment
+---- Service
+---- Configuration
+---- Secrets
+---- Identity
+---- Networking
+---- Authorization
+---- Observability
But I do not want to build everything at once.
The first step is only defining the API.
Starting With the Application CRD
I created a new Custom Resource Definition:
applications.platform.shubforge.dev
The API is:
Group: platform.shubforge.dev
Version: v1alpha1
Kind: Application
The resource is namespace scoped.
For now, the specification is intentionally small:
spec:
image: greeting-service:1.0.0
replicas: 1
port:
containerPort: 8080
That gives us only three concepts:
image
replicas
container port
I deliberately did not add things like:
CPU
memory
environment variables
Secrets
ConfigMaps
health checks
volumes
routes
Istio
authorization
API dependencies
yet.
Those can be introduced when the platform actually starts supporting them.
The CRD
The CRD currently looks roughly like this:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: applications.platform.shubforge.dev
spec:
group: platform.shubforge.dev
scope: Namespaced
names:
plural: applications
singular: application
kind: Application
shortNames:
- papp
versions:
- name: v1alpha1
served: true
storage: true
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- image
- port
properties:
image:
type: string
minLength: 1
replicas:
type: integer
minimum: 1
default: 1
port:
type: object
required:
- containerPort
properties:
containerPort:
type: integer
minimum: 1
maximum: 65535
The important thing here is that some validation belongs directly in the API.
For example:
replicas >= 1
and:
1 <= containerPort <= 65535
can be validated by Kubernetes before the controller even receives the resource.
Why port Is an Object
I could have used:
spec:
port: 8080
But I chose:
spec:
port:
containerPort: 8080
because it gives the API some room to grow.
Later it could become:
port:
containerPort: 8080
protocol: TCP
or eventually:
ports:
- name: http
containerPort: 8080
For now, I still want to keep it to one port.
This is v1alpha1, so the API can evolve as I understand the platform better.
Defining Status Early
There is no Application controller yet.
Still, I added the basic status shape I expect the platform operator to eventually populate.
Something like:
status:
observedGeneration: 1
deploymentName: greeting-service
serviceName: greeting-service
readyReplicas: 1
conditions:
- type: Ready
status: "True"
This gives the API an initial contract between:
desired state
and:
observed state
At the moment, the READY field remains empty because there is no controller.
That is expected.
Creating the First Application
I added a sample resource:
apiVersion: platform.shubforge.dev/v1alpha1
kind: Application
metadata:
name: greeting-service
spec:
image: greeting-service:1.0.0
replicas: 1
port:
containerPort: 8080
Then installed the CRD:
kubectl apply -f k8s/crds/applications.yaml
and created the Application:
kubectl apply -f k8s/samples/application.yaml
Now Kubernetes understands:
Application/greeting-service
I can check it using:
kubectl get applications
or the short name:
kubectl get papp
The output includes information such as:
NAME READY IMAGE REPLICAS
greeting-service greeting-service:1.0.0 1
READY is still empty because the platform operator does not exist yet.
That will be the next step.
Introducing Tests From the Beginning
One thing I want to do differently from the Greeting operator is testing.
I built several Greeting features first and only thought about automated tests later.
For the actual platform APIs, I want the flow to be:
feature
+
test
+
documentation
from the beginning.
There is no Java controller yet, so introducing JUnit, Awaitility, or operator test frameworks would be premature.
But the API itself can already be tested.
Testing the Application API
I added:
scripts/tests/application-api-test.sh
The test talks to the real Kubernetes API server.
It first installs the CRD and then uses:
kubectl apply --dry-run=server
to validate different Application manifests.
The important part is:
--dry-run=server
The resource is sent to the Kubernetes API server and validated there, but it is not persisted.
So the test is not implementing Kubernetes validation itself.
It is asking Kubernetes:
Would you accept this Application?
Testing a Valid Application
The first test checks the normal sample:
spec:
image: greeting-service:1.0.0
replicas: 1
port:
containerPort: 8080
Kubernetes should accept it.
Conceptually:
valid Application
|
v
Kubernetes API
|
v
accepted
Testing an Invalid Port
Next I test:
port:
containerPort: 70000
But the CRD defines:
maximum = 65535
So Kubernetes should reject it.
containerPort = 70000
|
v
CRD validation
|
v
rejected
Testing a Missing Image
The Application API requires:
image:
So this resource should fail:
spec:
replicas: 1
port:
containerPort: 8080
Again, Kubernetes handles the validation.
Testing Invalid Replicas
The API currently defines:
replicas >= 1
So this:
replicas: 0
should be rejected.
The test now verifies several parts of the Application contract:
valid resource
→ accepted
invalid port
→ rejected
missing image
→ rejected
replicas = 0
→ rejected
Running the API Tests
I added a Taskfile command:
task application:test:api
which runs:
scripts/tests/application-api-test.sh
The test output is roughly:
Testing Application CRD...
1. Installing Application CRD
✓ Application CRD installed
2. Testing valid Application
✓ Valid Application accepted
3. Testing invalid container port
✓ Invalid port rejected
4. Testing missing image
✓ Missing image rejected
5. Testing replicas below minimum
✓ Invalid replica count rejected
Application API tests passed.
This is simple, but it gives me the first automated test for the actual Platform Lab APIs.
Why I Am Not Using JUnit Yet
There is currently no Java code involved in the Application API.
So using something like:
JUnit
Awaitility
Java Operator SDK test utilities
Testcontainers
would not add much value yet.
Right now the thing I want to test is:
Does Kubernetes correctly understand and validate my platform API?
The real Kubernetes API server is the best place to answer that.
Once I build:
ApplicationReconciler
the test requirements will change.
Then I will want to verify things like:
Application created
|
v
Deployment created
Application created
|
v
Service created
That is where Java integration tests will start making sense.
Taskfile Commands
I also added Application-related commands to the Taskfile.
Install the CRD:
task application:crd:install
Create the sample Application:
task application:create
List Applications:
task application:get
Describe the sample:
task application:describe
Delete it:
task application:delete
Run API tests:
task application:test:api
The Taskfile continues to be the main developer interface for Platform Lab.
Where the Platform Is Now
The repository has moved from:
Greeting
|
v
learning Kubernetes operators
to:
Application
|
v
building the actual platform
Right now:
Application CRD
|
v
Kubernetes API
is all that exists.
The next step is:
Application
|
v
ApplicationReconciler
|
+---- Deployment
|
+---- Service
That will be the first actual platform behavior.
Source Code
The complete project is available in my Platform Lab repository.
Repository: Platform Lab
The changes from this post are available in:
Pull Request: Add Platform Application API
The main additions are:
Application CRD
Application sample
CRD validation
Application Taskfile commands
Application API tests
What I Learned
One thing I am trying to change as I move into the actual platform work is when I introduce complexity.
With the Greeting operator, I explored many Kubernetes concepts because the purpose was learning.
For the platform itself, I want features to have a reason to exist.
For example, I am not adding:
finalizers
advanced reconciliation
external API handling
Secrets
Istio configuration
until a real platform feature needs them.
The same applies to testing.
For this API, a small server-side Kubernetes validation test is enough.
When the controller arrives, the testing strategy can grow with it.
What's Next?
The next step is much more interesting.
I want this:
apiVersion: platform.shubforge.dev/v1alpha1
kind: Application
metadata:
name: greeting-service
spec:
image: greeting-service:1.0.0
replicas: 1
port:
containerPort: 8080
to produce:
Application/greeting-service
|
v
Platform Operator
/ \
v v
Deployment Service
That means the next step is building the actual:
platform-operator
and its first controller:
ApplicationReconciler
This time, the controller and its integration tests will be built together.
Top comments (0)