Source code for the Go operator talk at Voxxed Days Lux 2022
- la branche
01-init-project
contient le résultat de cette étape - installer / mettre à jour la dernière version du Operator SDK (v1.20.1 au moment de l'écriture du readme)
- créer le répertoire
voxxed-days-go-operator
- dans le répertoire
voxxed-days-go-operator
, scaffolding du projet :operator-sdk init --domain fr.wilda --repo github.com/philippart-s/voxxed-days-go-operator
- A ce stage une arborescence complète a été générée, notamment la partie configuration dans
config
et unMakefile
permettant le lancement des différentes commandes de build - vérification que cela compile :
go build
- la branche
02-crd-generation
contient le résultat de cette étape - création de l'API :
operator-sdk create api --version v1 --kind NginxOperator --resource --controller
- de nouveau, de nombreux fichiers de générés, notamment le controller
./controllers/nginxoperator_controller.go
package controllers
import (
"context"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
// ⚠️ Change the github repository name with your own repository name ⚠️
frwildav1 "github.com/philippart-s/voxxed-days-go-operator/api/v1"
)
// NginxOperatorReconciler reconciles a NginxOperator object
type NginxOperatorReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators/finalizers,verbs=update
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
// TODO(user): Modify the Reconcile function to compare the state specified by
// the NginxOperator object against the actual cluster state, and then
// perform operations to make the cluster state reflect the state specified by
// the user.
//
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
_ = log.FromContext(ctx)
// TODO(user): your logic here
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *NginxOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&frwildav1.NginxOperator{}).
Complete(r)
}
- ensuite on génère la CRD avec la commande
make manifests
:
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.8.0
creationTimestamp: null
name: nginxoperators.fr.wilda
spec:
group: fr.wilda
names:
kind: NginxOperator
listKind: NginxOperatorList
plural: nginxoperators
singular: nginxoperator
scope: Namespaced
versions:
- name: v1
schema:
openAPIV3Schema:
description: NginxOperator is the Schema for the nginxoperators API
properties:
apiVersion:
description: 'APIVersion defines the versioned schema of this representation
of an object. Servers should convert recognized schemas to the latest
internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources'
type: string
kind:
description: 'Kind is a string value representing the REST resource this
object represents. Servers may infer this from the endpoint the client
submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds'
type: string
metadata:
type: object
spec:
description: NginxOperatorSpec defines the desired state of NginxOperator
properties:
foo:
description: Foo is an example field of NginxOperator. Edit nginxoperator_types.go
to remove/update
type: string
type: object
status:
description: NginxOperatorStatus defines the observed state of NginxOperator
type: object
type: object
served: true
storage: true
subresources:
status: {}
status:
acceptedNames:
kind: ""
plural: ""
conditions: []
storedVersions: []
- puis on peut l'appliquer avec la commande
make install
- et vérfier qu'elle a été créée :
kubectl get crd nginxoperators.fr.wilda
kubectl get crd nginxoperators.fr.wilda
NAME CREATED AT
nginxoperators.fr.wilda 2022-06-07T09:42:46Z
- la branche
03-hello-world
contient le résultat de cette étape - ajouter un champ
name
dansapi/v1/nginxoperator_types.go
: ℹ️ les attributs sont définis en utilisant les JSON tags : https://pkg.go.dev/encoding/json#Marshal ℹ️
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NginxOperatorSpec defines the desired state of NginxOperator
type NginxOperatorSpec struct {
Name string `json:"name,omitempty"`
}
// NginxOperatorStatus defines the observed state of NginxOperator
type NginxOperatorStatus struct {
}
//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
// NginxOperator is the Schema for the nginxoperators API
type NginxOperator struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec NginxOperatorSpec `json:"spec,omitempty"`
Status NginxOperatorStatus `json:"status,omitempty"`
}
//+kubebuilder:object:root=true
// NginxOperatorList contains a list of NginxOperator
type NginxOperatorList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []NginxOperator `json:"items"`
}
func init() {
SchemeBuilder.Register(&NginxOperator{}, &NginxOperatorList{})
}
- lancer la commande
make manifests
pour générer le manifest de la CRD - mettre à jour la CRD :
make install
- vérifier que la CRD a bien été mise à jour:
$ kubectl get crds nginxoperators.fr.wilda -o json | jq '.spec.versions[0].schema.openAPIV3Schema.properties.spec'
{
"description": "NginxOperatorSpec defines the desired state of NginxOperator",
"properties": {
"name": {
"description": "Name to say hello",
"type": "string"
}
},
"type": "object"
}
- modifier le reconciler
controllers/nginxoperator_controller.go
:
package controllers
import (
"context"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/log"
frwildav1 "github.com/philippart-s/voxxed-days-go-operator/api/v1"
)
// NginxOperatorReconciler reconciles a NginxOperator object
type NginxOperatorReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators/finalizers,verbs=update
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
const helloWorldFinalizer = "fr.wilda/finalizer"
helloWorld := &frwildav1.NginxOperator{}
err := r.Get(ctx, req.NamespacedName, helloWorld)
if err != nil {
if errors.IsNotFound(err) {
// CR deleted, nothing to do
log.Info("No CR found, nothing to do 🧐.")
} else {
// Error reading the object - requeue the request.
log.Error(err, "Failed to get CR NginxOperator")
return ctrl.Result{}, err
}
} else {
// Add finalizer for this CR
if !controllerutil.ContainsFinalizer(helloWorld, helloWorldFinalizer) {
controllerutil.AddFinalizer(helloWorld, helloWorldFinalizer)
err = r.Update(ctx, helloWorld)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{}, nil
}
if helloWorld.GetDeletionTimestamp() != nil {
// CR marked for deletion ➡️ Goodbye
log.Info("Goodbye " + helloWorld.Spec.Name + " 😢")
controllerutil.RemoveFinalizer(helloWorld, helloWorldFinalizer)
err := r.Update(ctx, helloWorld)
if err != nil {
log.Info("Error during deletion")
return ctrl.Result{}, err
}
} else {
// CR created / updated ➡️ Hello
log.Info("Hello " + helloWorld.Spec.Name + " 🎉🎉 !!")
}
}
return ctrl.Result{}, nil
}
// SetupWithManager sets up the controller with the Manager.
func (r *NginxOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&frwildav1.NginxOperator{}).
Complete(r)
}
- vérifier que to compile
go build
- lancer l'opérateur sur la machine de dev :
make install run
:
$ make install run
/Users/stef/Talks/operators-for-all-dev/voxxed-days-2022/voxxed-days-go-operator/bin/controller-gen rbac:roleName=manager-role crd webhook paths="./..." output:crd:artifacts:config=config/crd/bases
/Users/stef/Talks/operators-for-all-dev/voxxed-days-2022/voxxed-days-go-operator/bin/kustomize build config/crd | kubectl apply -f -
customresourcedefinition.apiextensions.k8s.io/nginxoperators.fr.wilda configured
/Users/stef/Talks/operators-for-all-dev/voxxed-days-2022/voxxed-days-go-operator/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go run ./main.go
1.654608708040212e+09 INFO controller-runtime.metrics Metrics server is starting to listen {"addr": ":8080"}
1.6546087080409338e+09 INFO setup starting manager
1.6546087080413399e+09 INFO Starting server {"path": "/metrics", "kind": "metrics", "addr": "[::]:8080"}
1.6546087080414422e+09 INFO Starting server {"kind": "health probe", "addr": "[::]:8081"}
1.654608708041564e+09 INFO controller.nginxoperator Starting EventSource {"reconciler group": "fr.wilda", "reconciler kind": "NginxOperator", "source": "kind source: *v1.NginxOperator"}
1.654608708041604e+09 INFO controller.nginxoperator Starting Controller {"reconciler group": "fr.wilda", "reconciler kind": "NginxOperator"}
1.654608708143117e+09 INFO controller.nginxoperator Starting workers {"reconciler group": "fr.wilda", "reconciler kind": "NginxOperator", "worker count": 1}
- créer le namespace
test-helloworld-operator
:kubectl create ns test-helloworld-operator
- mettre à jour la CR
config/samples/_v1_nginxoperator.yaml
pour tester:
apiVersion: fr.wilda/v1
kind: NginxOperator
metadata:
name: nginxoperator-sample
spec:
name: Voxxed Days Lux
- créer la CR dans Kubernetes :
kubectl apply -f ./config/samples/_v1_nginxoperator.yaml -n test-helloworld-operator
- la sortie de l'opérateur devrait afficher le message
Hello Voxxed Days Lux 🎉🎉 !!
- supprimer la CR :
kubectl delete nginxoperators.fr.wilda/nginxoperator-sample -n test-helloworld-operator
- la sortie de l'opérateur devrait afficher le message
Goodbye Voxxed Days Lux 😢
- la branche
04-nginx-operator
contient le résultat de cette étape - modifier le fichier
api/v1/nginxoperator_types.go
:
package v1
import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NginxOperatorSpec defines the desired state of NginxOperator
type NginxOperatorSpec struct {
// Number of replicas for the Nginx Pods
ReplicaCount int32 `json:"replicaCount"`
// Exposed port for the Nginx server
Port int32 `json:"port"`
}
// Unchanged code
// ...
- générer la CRD modifiée :
make manifests
- deployer la CRD dans Kubernetes :
make install
- vérifier que la CRD a bien été mise à jour:
$ kubectl get crds nginxoperators.fr.wilda -o json | jq '.spec.versions[0].schema.openAPIV3Schema.properties.spec'
{
"description": "NginxOperatorSpec defines the desired state of NginxOperator",
"properties": {
"port": {
"description": "Exposed port for the Nginx server",
"format": "int32",
"type": "integer"
},
"replicaCount": {
"description": "Number of replicas for the Nginx Pods",
"format": "int32",
"type": "integer"
}
},
"required": [
"port",
"replicaCount"
],
"type": "object"
}
- modifier le reconciler en ajoutant les fonctions permettant la création d'un Deployment et d'un Service dans
controllers/nginxoperator_controller.go
:
// Create a Deployment for the Nginx server.
func (r *NginxOperatorReconciler) createDeployment(nginxCR *frwildav1.NginxOperator) *appsv1.Deployment {
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: nginxCR.Name,
Namespace: nginxCR.Namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: &nginxCR.Spec.ReplicaCount,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "nginx"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": "nginx"},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Image: "ovhplatform/hello:1.0",
Name: "nginx",
Ports: []corev1.ContainerPort{{
ContainerPort: 80,
Name: "http",
Protocol: "TCP",
}},
}},
},
},
},
}
return deployment
}
// Create a Service for the Nginx server.
func (r *NginxOperatorReconciler) createService(nginxCR *frwildav1.NginxOperator) *corev1.Service {
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: nginxCR.Name,
Namespace: nginxCR.Namespace,
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{
"app": "nginx",
},
Ports: []corev1.ServicePort{
{
Name: "http",
NodePort: nginxCR.Spec.Port,
Port: 80,
TargetPort: intstr.FromInt(80),
},
},
Type: corev1.ServiceTypeNodePort,
},
}
return service
}
- modifier le reconciler pour la création du Pod et de son Service dans
controllers/nginxoperator_controller.go
:
package controllers
import (
"context"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/intstr"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
frwildav1 "github.com/philippart-s/voxxed-days-go-operator/api/v1"
appsv1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// NginxOperatorReconciler reconciles a NginxOperator object
type NginxOperatorReconciler struct {
client.Client
Scheme *runtime.Scheme
}
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators/finalizers,verbs=update
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.0/pkg/reconcile
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
log.Info("⚡️ Event !!! ⚡️")
nginxCR := &frwildav1.NginxOperator{}
existingNginxDeployment := &appsv1.Deployment{}
existingService := &corev1.Service{}
err := r.Get(ctx, req.NamespacedName, nginxCR)
if err == nil {
// Check if the deployment already exists, if not: create a new one.
err = r.Get(ctx, types.NamespacedName{Name: nginxCR.Name, Namespace: nginxCR.Namespace}, existingNginxDeployment)
if err != nil && errors.IsNotFound(err) {
// Define a new deployment
newNginxDeployment := r.createDeployment(nginxCR)
log.Info("✨ Creating a new Deployment", "Deployment.Namespace", newNginxDeployment.Namespace, "Deployment.Name", newNginxDeployment.Name)
err = r.Create(ctx, newNginxDeployment)
if err != nil {
log.Error(err, "❌ Failed to create new Deployment", "Deployment.Namespace", newNginxDeployment.Namespace, "Deployment.Name", newNginxDeployment.Name)
return ctrl.Result{}, err
}
} else if err == nil {
// Deployment exists, check if the Deployment must be updated
var replicaCount int32 = nginxCR.Spec.ReplicaCount
if *existingNginxDeployment.Spec.Replicas != replicaCount {
log.Info("🔁 Number of replicas changes, update the deployment! 🔁")
existingNginxDeployment.Spec.Replicas = &replicaCount
err = r.Update(ctx, existingNginxDeployment)
if err != nil {
log.Error(err, "❌ Failed to update Deployment", "Deployment.Namespace", existingNginxDeployment.Namespace, "Deployment.Name", existingNginxDeployment.Name)
return ctrl.Result{}, err
}
}
}
// Check if the service already exists, if not: create a new one
err = r.Get(ctx, types.NamespacedName{Name: nginxCR.Name, Namespace: nginxCR.Namespace}, existingService)
if err != nil && errors.IsNotFound(err) {
// Create the Service
newService := r.createService(nginxCR)
log.Info("✨ Creating a new Service", "Service.Namespace", newService.Namespace, "Service.Name", newService.Name)
err = r.Create(ctx, newService)
if err != nil {
log.Error(err, "❌ Failed to create new Service", "Service.Namespace", newService.Namespace, "Service.Name", newService.Name)
return ctrl.Result{}, err
}
} else if err == nil {
// Service exists, check if the port have to be updated.
var port int32 = nginxCR.Spec.Port
if existingService.Spec.Ports[0].NodePort != port {
log.Info("🔁 Port number changes, update the service! 🔁")
existingService.Spec.Ports[0].NodePort = port
err = r.Update(ctx, existingService)
if err != nil {
log.Error(err, "❌ Failed to update Service", "Service.Namespace", existingService.Namespace, "Service.Name", existingService.Name)
return ctrl.Result{}, err
}
}
} else if err != nil {
log.Error(err, "Failed to get Service")
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// ... unmodified code
- vérifier que ça compile :
go build
- créer le namespace
test-nginx-operator
:kubectl create ns test-nginx-operator
- créer la CR:
config/samples/_v2_nginxoperator.yaml
:
apiVersion: fr.wilda/v1
kind: NginxOperator
metadata:
name: nginxoperator-sample
spec:
replicaCount: 1
port: 30080
- lancer l'opérateur en mode dev :
make install run
- puis créer la CR sur Kubernetes:
kubectl apply -f ./config/samples/_v2_nginxoperator.yaml -n test-nginx-operator
- l'opérateur devrait créer le pod Nginx et son service:
INFO controller.nginxoperator ⚡️ Event !!! ⚡️ {"reconciler group": "fr.wilda", "reconciler kind": "NginxOperator", "name": "nginxoperator-sample", "namespace": "test-nginx-operator"}
INFO controller.nginxoperator ✨ Creating a new Deployment {"reconciler group": "fr.wilda", "reconciler kind": "NginxOperator", "name": "nginxoperator-sample", "namespace": "test-nginx-operator", "Deployment.Namespace": "test-nginx-operator", "Deployment.Name": "nginxoperator-sample"}
Dans Kubernetes:
$ kubectl get pod,svc,nginxoperator -n test-nginx-operator
NAME READY STATUS RESTARTS AGE
pod/nginxoperator-sample-58c4f478ff-phz7t 1/1 Running 0 17s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginxoperator-sample NodePort 10.3.173.89 <none> 80:30080/TCP 17s
NAME AGE
nginxoperator.fr.wilda/nginxoperator-sample 17s
- tester dans un navigateur ou par un curl l'accès à
http://<node external ip>:30080
, pour récupérer l'IP externe du node :kubectl cluster-info
- la branche
05-update-cr
contient le résultat de cette étape - changer le port et le nombre de replicas dans la CR
config/samples/_v2_nginxoperator.yaml
:
apiVersion: fr.wilda/v1
kind: NginxOperator
metadata:
name: nginxoperator-sample
spec:
replicaCount: 2
port: 30081
- appliquer la CR:
kubectl apply -f ./config/samples/_v2_nginxoperator.yaml -n test-nginx-operator
- vérifier que le nombre de pods et le port ont bien changés:
$ kubectl get pod,svc -n test-nginx-operator
NAME READY STATUS RESTARTS AGE
pod/nginxoperator-sample-58c4f478ff-l2gwz 1/1 Running 0 10s
pod/nginxoperator-sample-58c4f478ff-phz7t 1/1 Running 0 9m41s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginxoperator-sample NodePort 10.3.173.89 <none> 80:30081/TCP 9m41s
- tester dans un navigateur ou par un curl l'accès à
http://<node external ip>:30081
- supprimer la CR :
kubectl delete nginxoperators.fr.wilda/nginxoperator-sample -n test-nginx-operator
- vérifier que rien a été supprimé:
$ kubectl get pod,svc -n test-nginx-operator
NAME READY STATUS RESTARTS AGE
pod/nginxoperator-sample-58c4f478ff-l2gwz 1/1 Running 0 2m23s
pod/nginxoperator-sample-58c4f478ff-phz7t 1/1 Running 0 11m
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginxoperator-sample NodePort 10.3.173.89 <none> 80:30081/TCP 11m
- supprimer le Deployment :
kubectl delete deployment nginxoperator-sample -n test-nginx-operator
- supprimer le Service :
kubectl delete service nginxoperator-sample -n test-nginx-operator
- la branche
06-watch-deletion
contient le résultat de cette étape - modifier le reconciler
controllers/nginxoperator_controller.go
pour que le Service et le Deployment soient gérés en suppression:
// unmodified code ...
// Create a Deployment for the Nginx server.
func (r *NginxOperatorReconciler) createDeployment(nginxCR *frwildav1.NginxOperator) *appsv1.Deployment {
deployment := &appsv1.Deployment{
ObjectMeta: metav1.ObjectMeta{
Name: nginxCR.Name,
Namespace: nginxCR.Namespace,
},
Spec: appsv1.DeploymentSpec{
Replicas: &nginxCR.Spec.ReplicaCount,
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{"app": "nginx"},
},
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"app": "nginx"},
},
Spec: corev1.PodSpec{
Containers: []corev1.Container{{
Image: "ovhplatform/hello:1.0",
Name: "nginx",
Ports: []corev1.ContainerPort{{
ContainerPort: 80,
Name: "http",
Protocol: "TCP",
}},
}},
},
},
},
}
// Set nginxCR instance as the owner and controller
ctrl.SetControllerReference(nginxCR, deployment, r.Scheme)
return deployment
}
// Create a Service for the Nginx server.
func (r *NginxOperatorReconciler) createService(nginxCR *frwildav1.NginxOperator) *corev1.Service {
service := &corev1.Service{
ObjectMeta: metav1.ObjectMeta{
Name: nginxCR.Name,
Namespace: nginxCR.Namespace,
},
Spec: corev1.ServiceSpec{
Selector: map[string]string{
"app": "nginx",
},
Ports: []corev1.ServicePort{
{
Name: "http",
NodePort: nginxCR.Spec.Port,
Port: 80,
TargetPort: intstr.FromInt(80),
},
},
Type: corev1.ServiceTypeNodePort,
},
}
// Set nginxCR instance as the owner and controller
ctrl.SetControllerReference(nginxCR, service, r.Scheme)
return service
}
// unmodified code ...
- lancer l'opérateur en mode dev :
make install run
- appliquer la CR:
kubectl apply -f ./config/samples/_v2_nginxoperator.yaml -n test-nginx-operator
- vérifier que tout est créé:
$ kubectl get pod,svc -n test-nginx-operator
NAME READY STATUS RESTARTS AGE
pod/nginxoperator-sample-58c4f478ff-cqm6f 1/1 Running 0 13s
pod/nginxoperator-sample-58c4f478ff-xvs4h 1/1 Running 0 13s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginxoperator-sample NodePort 10.3.165.48 <none> 80:30081/TCP 13s
- supprimer la CR :
kubectl delete nginxoperators.fr.wilda/nginxoperator-sample -n test-nginx-operator
- vérifier que tout est supprimé:
$ kubectl get pod,svc -n test-nginx-operator
No resources found in test-nginx-operator namespace.
- la branche
07-watch-service-deletion
contient le résultat de cette étape - utiliser la CR de tests
config/sample/_v2_nginxoperator.yaml
pour créer tous les éléments :kubectl apply -f ./config/samples/_v2_nginxoperator.yaml -n test-nginx-operator
- supprimer le service :
kubectl delete svc/nginxoperator-sample -n test-nginx-operator
- constater qu'il n'est pas recréé:
kubectl get svc -n test-nginx-operator
$ kubectl get svc -n test-nginx-operator
No resources found in test-nginx-operator namespace.
- supprimer la CR :
kubectl delete nginxoperators.fr.wilda/nginxoperator-sample -n test-nginx-operator
- modifier le reconciler
controllers/nginxoperator_controller.go
pour qu'il surveille le service:
// unmodified code ...
// SetupWithManager sets up the controller with the Manager.
func (r *NginxOperatorReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&frwildav1.NginxOperator{}).
// Enable service Watching
Owns(&corev1.Service{}).
Complete(r)
}
- lancer l'opérateur en mode dev :
make install run
- appliquer la CR de tests
config/sample/_v2_nginxoperator.yaml
pour créer tous les éléments :kubectl apply -f ./config/samples/_v2_nginxoperator.yaml -n test-nginx-operator
- supprimer le service :
kubectl delete svc/nginxoperator-sample -n test-nginx-operator
- l'opérateur le recrée et il devrait réapparaître :
INFO controller.nginxoperator ⚡️ Event !!! ⚡️ {"reconciler group": "fr.wilda", "reconciler kind": "NginxOperator", "name": "nginxoperator-sample", "namespace": "test-nginx-operator"}
INFO controller.nginxoperator ✨ Creating a new Service {"reconciler group": "fr.wilda", "reconciler kind": "NginxOperator", "name": "nginxoperator-sample", "namespace": "test-nginx-operator", "Service.Namespace": "test-nginx-operator", "Service.Name": "nginxoperator-sample"}
kubectl get pod,svc -n test-nginx-operator
NAME READY STATUS RESTARTS AGE
pod/nginxoperator-sample-58c4f478ff-ctd8b 1/1 Running 0 54s
pod/nginxoperator-sample-58c4f478ff-rtgqg 1/1 Running 0 54s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginxoperator-sample NodePort 10.3.245.149 <none> 80:30081/TCP 26s
- la branche
08-add-limit-to-replicas
contient le résultat de cette étape - modifier le reconciler
nginxoperator_controller.go
pour qu'il empêche la création de plus de 3 Pods:
// unmodified code ...
func (r *NginxOperatorReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
log.Info("⚡️ Event !!! ⚡️")
nginxCR := &frwildav1.NginxOperator{}
existingNginxDeployment := &appsv1.Deployment{}
existingService := &corev1.Service{}
err := r.Get(ctx, req.NamespacedName, nginxCR)
if err == nil {
var replicaCount int32 = nginxCR.Spec.ReplicaCount
if replicaCount < 1 || replicaCount > 2 {
log.Info("🛑 An invalid number of replicas is set (must be 1 or 2) 🛑", "replica number", replicaCount)
replicaCount = 1
}
// Check if the deployment already exists, if not: create a new one.
err = r.Get(ctx, types.NamespacedName{Name: nginxCR.Name, Namespace: nginxCR.Namespace}, existingNginxDeployment)
if err != nil && errors.IsNotFound(err) {
// Define a new deployment
newNginxDeployment := r.createDeployment(nginxCR)
log.Info("✨ Creating a new Deployment", "Deployment.Namespace", newNginxDeployment.Namespace, "Deployment.Name", newNginxDeployment.Name)
err = r.Create(ctx, newNginxDeployment)
if err != nil {
log.Error(err, "❌ Failed to create new Deployment", "Deployment.Namespace", newNginxDeployment.Namespace, "Deployment.Name", newNginxDeployment.Name)
return ctrl.Result{}, err
}
} else if err == nil {
// Deployment exists, check if the Deployment must be updated
if *existingNginxDeployment.Spec.Replicas != replicaCount {
log.Info("🔁 Number of replicas changes, update the deployment! 🔁")
existingNginxDeployment.Spec.Replicas = &replicaCount
err = r.Update(ctx, existingNginxDeployment)
if err != nil {
log.Error(err, "❌ Failed to update Deployment", "Deployment.Namespace", existingNginxDeployment.Namespace, "Deployment.Name", existingNginxDeployment.Name)
return ctrl.Result{}, err
}
}
}
// Check if the service already exists, if not: create a new one
err = r.Get(ctx, types.NamespacedName{Name: nginxCR.Name, Namespace: nginxCR.Namespace}, existingService)
if err != nil && errors.IsNotFound(err) {
// Create the Service
newService := r.createService(nginxCR)
log.Info("✨ Creating a new Service", "Service.Namespace", newService.Namespace, "Service.Name", newService.Name)
err = r.Create(ctx, newService)
if err != nil {
log.Error(err, "❌ Failed to create new Service", "Service.Namespace", newService.Namespace, "Service.Name", newService.Name)
return ctrl.Result{}, err
}
} else if err == nil {
// Service exists, check if the port have to be updated.
var port int32 = nginxCR.Spec.Port
if existingService.Spec.Ports[0].NodePort != port {
log.Info("🔁 Port number changes, update the service! 🔁")
existingService.Spec.Ports[0].NodePort = port
err = r.Update(ctx, existingService)
if err != nil {
log.Error(err, "❌ Failed to update Service", "Service.Namespace", existingService.Namespace, "Service.Name", existingService.Name)
return ctrl.Result{}, err
}
}
} else if err != nil {
log.Error(err, "Failed to get Service")
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
// unmodified code ...
- changer le nombre de replicas dans la CR
_v2_nginxoperator.yaml
:
apiVersion: "fr.wilda/v1"
kind: NginxOperator
metadata:
name: nginxoperator-sample
spec:
replicaCount: 5
port: 30081
- appliquer la CR:
kubectl apply -f ./config/samples/_v2_nginxoperator.yaml -n test-nginx-operator
- vérifier que le nombre de pods n'a pas changé:
$ kubectl get pod -n test-nginx-operator
NAME READY STATUS RESTARTS AGE
nginxoperator-sample-58c4f478ff-jjlnq 1/1 Running 0 11s
nginxoperator-sample-58c4f478ff-z59dx 1/1 Running 0 11s
- et que l'opérateur a affiché le message d'erreur indiquant que le nombre de pods est invalide:
INFO controller.nginxoperator 🛑 An invalid number of replicas is set (must be 1 or 2) 🛑 {"reconciler group": "fr.wilda", "reconciler kind": "NginxOperator", "name": "nginxoperator-sample", "namespace": "test-nginx-operator", "replica number": 5}
- supprimer la CR:
kubectl delete nginxoperators.fr.wilda/nginxoperator-sample -n test-nginx-operator
- la branche
09-package-deploy
contient le résultat de cette étape - modifier le controller
controllers/nginxoperator_controller.go
pour les droits:
// unmodified code ...
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=fr.wilda,resources=nginxoperators/finalizers,verbs=update
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups="",resources=services,verbs=get;list;watch;create;update;patch;delete
// unmodified code ...
- générer les RBAC dans
config/rbac/role.yaml
:make manifests
- modifier le Makefile:
## unmodified code ...
IMAGE_TAG_BASE ?= wilda/voxxed-days-go-operator
## unmodified code ...
IMG ?= $(IMAGE_TAG_BASE):$(VERSION)
## unmodified code ...
.PHONY: docker-build
docker-build: #test ## Build docker image with the manager.
docker build -t ${IMG} .
## unmodified code ...
- lancer la création de l'image:
make docker-build
- s'authentifier sur le docker hub :
docker login
- push de l'image :
make docker-push
:
$ make docker-push
docker push wilda/voxxed-days-go-operator:0.0.1
The push refers to repository [docker.io/wilda/voxxed-days-go-operator]
4ae86d9be536: Pushed
798afb9dcee7: Mounted from wilda/voxxed-days-go-operator
0.0.1: digest: sha256:6fa3ab35c7b93e901f64a2415e7cae00f36dc657ee9fd81a11827a8d38a24421 size: 739
- déployer l'opérateur dans Kubernetes :
make deploy
:
$ kubectl get deployment -n voxxed-days-go-operator-system
NAME READY UP-TO-DATE AVAILABLE AGE
voxxed-days-go-operator-controller-manager 1/1 1 1 79s
- créer la CR :
kubectl apply -f ./config/samples/_v2_nginxoperator.yaml -n test-nginx-operator
- vérifier que l'opérateur a fait le nécessaire:
kubectl get pod,svc -n test-nginx-operator
$ kubectl get pod,svc -n test-nginx-operator
NAME READY STATUS RESTARTS AGE
pod/nginxoperator-sample-58c4f478ff-twqpj 1/1 Running 0 11s
pod/nginxoperator-sample-58c4f478ff-zngms 1/1 Running 0 11s
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/nginxoperator-sample NodePort 10.3.206.172 <none> 80:30081/TCP 11s
- supprimer la CR :
kubectl delete nginxoperators.fr.wilda/nginxoperator-sample -n test-nginx-operator
- undeploy de l'opérateur :
make undeploy
- supprimer les namespaces:
kubectl delete ns test-nginx-operator test-helloworld-operator