Переглянути джерело

postgresql with service (nodeport)

iwanhae 11 місяців тому
батько
коміт
56a2f7481f

+ 29 - 2
api/v1/postgresql_types.go

@@ -37,21 +37,48 @@ type PostgreSQLSpec struct {
 
 // PostgreSQLStatus defines the observed state of PostgreSQL
 type PostgreSQLStatus struct {
-	Status Status `json:"status"`
+	Conditions Conditions `json:"conditions,omitempty"`
+	Status     Status     `json:"status,omitempty"`
+	ListenOn   Listen     `json:"listenOn,omitempty"`
+}
+
+type Listen struct {
+	Node string `json:"node,omitempty"`
+	Host string `json:"host,omitempty"`
+	Port int32  `json:"port,omitempty"`
+}
+
+type Conditions struct {
+	Pod     Status `json:"pod,omitempty"`
+	Service Status `json:"service,omitempty"`
 }
 
 type Status string
 
 const (
+	Status_Error        Status = "error"
 	Status_Pending      Status = "pending"
 	Status_Initializing Status = "initializing"
 	Status_NotReady     Status = "notReady"
 	Status_Ready        Status = "ready"
-	Status_Error        Status = "error"
 )
 
+func (a Status) IsLowerThan(b Status) bool {
+	var m = map[Status]int{
+		Status_Error:        0,
+		Status_Pending:      1,
+		Status_Initializing: 2,
+		Status_NotReady:     3,
+		Status_Ready:        4,
+	}
+	return m[a] < m[b]
+}
+
 //+kubebuilder:object:root=true
 //+kubebuilder:subresource:status
+//+kubebuilder:printcolumn:JSONPath=".status.listenOn.node",name=Node,type=string
+//+kubebuilder:printcolumn:JSONPath=".status.listenOn.host",name=Host,type=string
+//+kubebuilder:printcolumn:JSONPath=".status.listenOn.port",name=Port,type=number
 //+kubebuilder:printcolumn:JSONPath=".status.status",name=Status,type=string
 
 // PostgreSQL is the Schema for the postgresqls API

+ 32 - 0
api/v1/zz_generated.deepcopy.go

@@ -24,6 +24,36 @@ import (
 	runtime "k8s.io/apimachinery/pkg/runtime"
 )
 
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Conditions) DeepCopyInto(out *Conditions) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Conditions.
+func (in *Conditions) DeepCopy() *Conditions {
+	if in == nil {
+		return nil
+	}
+	out := new(Conditions)
+	in.DeepCopyInto(out)
+	return out
+}
+
+// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
+func (in *Listen) DeepCopyInto(out *Listen) {
+	*out = *in
+}
+
+// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Listen.
+func (in *Listen) DeepCopy() *Listen {
+	if in == nil {
+		return nil
+	}
+	out := new(Listen)
+	in.DeepCopyInto(out)
+	return out
+}
+
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PostgreSQL) DeepCopyInto(out *PostgreSQL) {
 	*out = *in
@@ -101,6 +131,8 @@ func (in *PostgreSQLSpec) DeepCopy() *PostgreSQLSpec {
 // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
 func (in *PostgreSQLStatus) DeepCopyInto(out *PostgreSQLStatus) {
 	*out = *in
+	out.Conditions = in.Conditions
+	out.ListenOn = in.ListenOn
 }
 
 // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PostgreSQLStatus.

+ 8 - 0
cmd/main.go

@@ -92,6 +92,14 @@ func main() {
 		os.Exit(1)
 	}
 
+	if err = (&controller.PostgreSQLServiceReconciler{
+		Client: mgr.GetClient(),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "PostgreSQLService")
+		os.Exit(1)
+	}
+
 	if err = (&controller.PostgreSQLPodReconciler{
 		Client: mgr.GetClient(),
 		Scheme: mgr.GetScheme(),

+ 26 - 2
config/crd/bases/database.iwanhae.kr_postgresqls.yaml

@@ -15,6 +15,15 @@ spec:
   scope: Namespaced
   versions:
   - additionalPrinterColumns:
+    - jsonPath: .status.listenOn.node
+      name: Node
+      type: string
+    - jsonPath: .status.listenOn.host
+      name: Host
+      type: string
+    - jsonPath: .status.listenOn.port
+      name: Port
+      type: number
     - jsonPath: .status.status
       name: Status
       type: string
@@ -59,10 +68,25 @@ spec:
           status:
             description: PostgreSQLStatus defines the observed state of PostgreSQL
             properties:
+              conditions:
+                properties:
+                  pod:
+                    type: string
+                  service:
+                    type: string
+                type: object
+              listenOn:
+                properties:
+                  host:
+                    type: string
+                  node:
+                    type: string
+                  port:
+                    format: int32
+                    type: integer
+                type: object
               status:
                 type: string
-            required:
-            - status
             type: object
         type: object
     served: true

+ 7 - 0
config/rbac/role.yaml

@@ -12,6 +12,13 @@ rules:
   - create
   - get
   - watch
+- apiGroups:
+  - ""
+  resources:
+  - services
+  verbs:
+  - get
+  - watch
 - apiGroups:
   - database.iwanhae.kr
   resources:

+ 91 - 22
internal/controller/postgresql_controller.go

@@ -19,17 +19,20 @@ package controller
 import (
 	"context"
 
+	databasev1 "github.com/iwanhae/nodb/api/v1"
+	"github.com/iwanhae/nodb/internal/templates"
+	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"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/controller/controllerutil"
 	"sigs.k8s.io/controller-runtime/pkg/log"
-
-	databasev1 "github.com/iwanhae/nodb/api/v1"
-	"github.com/iwanhae/nodb/internal/templates"
-	"github.com/pkg/errors"
-	corev1 "k8s.io/api/core/v1"
 )
 
 // PostgreSQLReconciler reconciles a PostgreSQL object
@@ -52,7 +55,7 @@ type PostgreSQLReconciler struct {
 //
 // For more details, check Reconcile and its Result here:
 // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.0/pkg/reconcile
-func (r *PostgreSQLReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+func (r *PostgreSQLReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
 	logger := log.FromContext(ctx)
 
 	obj := databasev1.PostgreSQL{}
@@ -63,20 +66,91 @@ func (r *PostgreSQLReconciler) Reconcile(ctx context.Context, req ctrl.Request)
 		logger.Error(err, "resource not found")
 		return ctrl.Result{}, err
 	}
-
+	original := obj.DeepCopy()
 	logger.Info("reconcile", "namespace", obj.Namespace, "name", obj.Name)
 
-	// Pending: Creating Pod if not exists
-	// do not handle spec update
+	defer func() {
+		status := databasev1.Status_Ready
+		if obj.Status.Conditions.Pod.IsLowerThan(status) {
+			status = obj.Status.Conditions.Pod
+		}
+		if obj.Status.Conditions.Service.IsLowerThan(status) {
+			status = obj.Status.Conditions.Service
+		}
+		obj.Status.Status = status
+		err = r.Status().Patch(ctx, &obj, client.MergeFrom(original))
+		if err != nil {
+			logger.Error(err, "failed to update status")
+		}
+	}()
+
+	if err := r.createOrUpdatePod(ctx, &obj); err != nil {
+		return ctrl.Result{}, err
+	}
+
+	if err := r.createOrUpdateService(ctx, &obj); err != nil {
+		return ctrl.Result{}, err
+	}
+
+	return ctrl.Result{}, nil
+}
+
+func (r *PostgreSQLReconciler) createOrUpdateService(ctx context.Context, obj *databasev1.PostgreSQL) error {
+	logger := log.FromContext(ctx)
+
+	svc := corev1.Service{
+		ObjectMeta: metav1.ObjectMeta{Name: obj.Name, Namespace: obj.Namespace},
+	}
+	err := r.Client.Get(ctx, types.NamespacedName{
+		Namespace: obj.Namespace, Name: obj.Name,
+	}, &svc)
+	if err != nil && !apierrors.IsNotFound(err) {
+		// can't handle other than not found error
+		return err
+	}
+
+	logger.Info("create or update service")
+	if result, err := controllerutil.CreateOrUpdate(ctx, r.Client, &svc, func() error {
+		if err := controllerutil.SetOwnerReference(obj, &svc, r.Scheme); err != nil {
+			return errors.Wrap(err, "failed to set owner reference")
+		}
+		svc.ObjectMeta.Labels = map[string]string{
+			templates.LabelKeyType: templates.LabelValuePostgreSQL,
+			templates.LabelKeyName: obj.Name,
+		}
+		svc.Spec.Selector = map[string]string{
+			templates.LabelKeyType: templates.LabelValuePostgreSQL,
+			templates.LabelKeyName: obj.Name,
+		}
+		svc.Spec.Ports = []corev1.ServicePort{
+			{Name: "postgres", Port: 5432, TargetPort: intstr.FromString("postgres"), Protocol: corev1.ProtocolTCP},
+		}
+		svc.Spec.Type = corev1.ServiceTypeNodePort
+		svc.Spec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicyLocal
+		return nil
+	}); err != nil {
+		return err
+	} else if result != controllerutil.OperationResultNone {
+		logger.Info("service modified", "result", result)
+	}
+
+	obj.Status.Conditions.Service = databasev1.Status_Ready
+	return nil
+}
+func (r *PostgreSQLReconciler) createOrUpdatePod(ctx context.Context, obj *databasev1.PostgreSQL) error {
+	logger := log.FromContext(ctx)
 
 	pod := corev1.Pod{}
-	if err := r.Client.Get(ctx, req.NamespacedName, &pod); err == nil {
+	if err := r.Client.Get(ctx, types.NamespacedName{
+		Namespace: obj.Namespace,
+		Name:      obj.Name,
+	}, &pod); err == nil {
 		// if found return
 		logger.Info("ignore already existing pod")
-		return ctrl.Result{}, nil
+		return nil
 	} else if !apierrors.IsNotFound(err) {
-		// weird error
-		return ctrl.Result{}, err
+		// can't handle other than not found error
+		return err
 	}
 
 	pod = templates.PostgreSQLPod(templates.PostgreSQLOpts{
@@ -89,21 +163,16 @@ func (r *PostgreSQLReconciler) Reconcile(ctx context.Context, req ctrl.Request)
 		Database: obj.Spec.Database,
 
 		Memory: resource.MustParse("1Gi"),
-		Owner:  &obj,
+		Owner:  obj,
 	})
 
 	logger.Info("create pod", "namespace", pod.Namespace, "name", pod.Name)
 	if err := r.Client.Create(ctx, &pod); err != nil {
-		return ctrl.Result{}, errors.Wrap(err, "failed to create pod")
+		return errors.Wrap(err, "failed to create pod")
 	}
 
-	obj.Status.Status = databasev1.Status_Initializing
-	logger.Info("update status")
-	if err := r.Status().Update(ctx, &obj); err != nil {
-		return ctrl.Result{}, errors.Wrap(err, "failed to patch status")
-	}
-
-	return ctrl.Result{}, nil
+	obj.Status.Conditions.Pod = databasev1.Status_Initializing
+	return nil
 }
 
 // SetupWithManager sets up the controller with the Manager.

+ 10 - 4
internal/controller/postgresql_pod_controller.go

@@ -51,10 +51,11 @@ func (r *PostgreSQLPodReconciler) Reconcile(ctx context.Context, req ctrl.Reques
 		logger.Error(err, "resource not found")
 		return ctrl.Result{}, err
 	}
+	original := obj.DeepCopy()
 
 	// will update Status anyway
 	defer func() {
-		err = r.Status().Update(ctx, &obj)
+		err = r.Status().Patch(ctx, &obj, client.MergeFrom(original))
 		if err != nil {
 			logger.Error(err, "failed to update status")
 		}
@@ -81,11 +82,16 @@ func (r *PostgreSQLPodReconciler) Reconcile(ctx context.Context, req ctrl.Reques
 
 	cond := podutil.GetPodReadyCondition(pod.Status)
 	if cond == nil {
-		obj.Status.Status = databasev1.Status_Initializing
+		obj.Status.Conditions.Pod = databasev1.Status_Initializing
 	} else if cond.Status == corev1.ConditionTrue {
-		obj.Status.Status = databasev1.Status_Ready
+		obj.Status.Conditions.Pod = databasev1.Status_Ready
 	} else {
-		obj.Status.Status = databasev1.Status_NotReady
+		obj.Status.Conditions.Pod = databasev1.Status_NotReady
+	}
+
+	if pod.Spec.NodeName != "" {
+		obj.Status.ListenOn.Node = pod.Spec.NodeName
+		obj.Status.ListenOn.Host = pod.Status.HostIP
 	}
 
 	return ctrl.Result{}, nil

+ 90 - 0
internal/controller/postgresql_svc_controller.go

@@ -0,0 +1,90 @@
+/*
+Copyright 2023.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+package controller
+
+import (
+	"context"
+
+	"github.com/iwanhae/nodb/internal/templates"
+	"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"
+
+	databasev1 "github.com/iwanhae/nodb/api/v1"
+	corev1 "k8s.io/api/core/v1"
+)
+
+// PostgreSQLReconciler reconciles a PostgreSQL object
+type PostgreSQLServiceReconciler struct {
+	client.Client
+	Scheme *runtime.Scheme
+}
+
+//+kubebuilder:rbac:groups="",resources=services,verbs=watch;get
+//+kubebuilder:rbac:groups=database.iwanhae.kr,resources=postgresqls/status,verbs=get;update;patch
+
+func (r *PostgreSQLServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (result ctrl.Result, err error) {
+	logger := log.FromContext(ctx)
+
+	// Find PostgreSQL
+	obj := databasev1.PostgreSQL{}
+	if err := r.Get(ctx, req.NamespacedName, &obj); err != nil {
+		if client.IgnoreNotFound(err) == nil {
+			return ctrl.Result{}, nil
+		}
+		logger.Error(err, "resource not found")
+		return ctrl.Result{}, err
+	}
+	original := obj.DeepCopy()
+
+	// will update Status anyway
+	defer func() {
+		err = r.Status().Patch(ctx, &obj, client.MergeFrom(original))
+		if err != nil {
+			logger.Error(err, "failed to update status")
+		}
+	}()
+
+	// Find Svc
+	svc := corev1.Service{}
+	if err := r.Get(ctx, req.NamespacedName, &svc); err != nil {
+		if client.IgnoreNotFound(err) == nil {
+			// Have PostgreSQL, but no pod
+			obj.Status.Status = databasev1.Status_Error
+			return ctrl.Result{}, nil
+		}
+		logger.Error(err, "resource not found")
+		return ctrl.Result{}, err
+	}
+
+	if svc.Labels[templates.LabelKeyType] != templates.LabelValuePostgreSQL {
+		// This svc is not for PostgreSQL
+		return ctrl.Result{}, nil
+	}
+
+	logger.Info("reconcile", "namespace", svc.Namespace, "name", svc.Name)
+	obj.Status.ListenOn.Port = svc.Spec.Ports[0].NodePort
+	return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *PostgreSQLServiceReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&corev1.Service{}).
+		Complete(r)
+}

+ 12 - 0
internal/templates/postgresql.go

@@ -12,6 +12,7 @@ import (
 
 const (
 	LabelKeyType         = "nodb.iwanhae.kr/type"
+	LabelKeyName         = "nodb.iwanhae.kr/name"
 	LabelValuePostgreSQL = "postgresql"
 )
 
@@ -36,6 +37,7 @@ func PostgreSQLPod(opts PostgreSQLOpts) corev1.Pod {
 			Name:      opts.Name,
 			Namespace: opts.Namespace,
 			Labels: map[string]string{
+				LabelKeyName: opts.Name,
 				LabelKeyType: LabelValuePostgreSQL,
 			},
 		},
@@ -54,6 +56,16 @@ func PostgreSQLPod(opts PostgreSQLOpts) corev1.Pod {
 						{Name: "POSTGRES_DB", Value: opts.Database},
 						{Name: "PGDATA", Value: "/pgdata"},
 					},
+					ResizePolicy: []corev1.ContainerResizePolicy{
+						{
+							ResourceName:  corev1.ResourceCPU,
+							RestartPolicy: corev1.NotRequired,
+						},
+						{
+							ResourceName:  corev1.ResourceMemory,
+							RestartPolicy: corev1.RestartContainer,
+						},
+					},
 					Resources: corev1.ResourceRequirements{
 						Limits: corev1.ResourceList{
 							corev1.ResourceMemory: opts.Memory,