Sfoglia il codice sorgente

basic functionality

iwanhae 11 mesi fa
parent
commit
03b03b52a4

+ 20 - 7
api/v1/postgresql_types.go

@@ -25,21 +25,34 @@ import (
 
 // PostgreSQLSpec defines the desired state of PostgreSQL
 type PostgreSQLSpec struct {
-	// INSERT ADDITIONAL SPEC FIELDS - desired state of cluster
-	// Important: Run "make" to regenerate code after modifying this file
-
-	// Foo is an example field of PostgreSQL. Edit postgresql_types.go to remove/update
-	Foo string `json:"foo,omitempty"`
+	// Version of Postgres to deploy. [16.0]
+	Version string `json:"version"`
+	// Username for authentication
+	User string `json:"user"`
+	// Password for authentication
+	Password string `json:"password"`
+	// Autogenerated name for default Database
+	Database string `json:"database"`
 }
 
 // PostgreSQLStatus defines the observed state of PostgreSQL
 type PostgreSQLStatus struct {
-	// INSERT ADDITIONAL STATUS FIELD - define observed state of cluster
-	// Important: Run "make" to regenerate code after modifying this file
+	Status Status `json:"status"`
 }
 
+type Status string
+
+const (
+	Status_Pending      Status = "pending"
+	Status_Initializing Status = "initializing"
+	Status_NotReady     Status = "notReady"
+	Status_Ready        Status = "ready"
+	Status_Error        Status = "error"
+)
+
 //+kubebuilder:object:root=true
 //+kubebuilder:subresource:status
+//+kubebuilder:printcolumn:JSONPath=".status.status",name=Status,type=string
 
 // PostgreSQL is the Schema for the postgresqls API
 type PostgreSQL struct {

+ 11 - 0
cmd/main.go

@@ -34,6 +34,7 @@ import (
 
 	databasev1 "github.com/iwanhae/nodb/api/v1"
 	"github.com/iwanhae/nodb/internal/controller"
+	"github.com/iwanhae/nodb/internal/templates"
 	//+kubebuilder:scaffold:imports
 )
 
@@ -46,6 +47,8 @@ func init() {
 	utilruntime.Must(clientgoscheme.AddToScheme(scheme))
 
 	utilruntime.Must(databasev1.AddToScheme(scheme))
+
+	templates.SetScheme(scheme)
 	//+kubebuilder:scaffold:scheme
 }
 
@@ -89,6 +92,14 @@ func main() {
 		os.Exit(1)
 	}
 
+	if err = (&controller.PostgreSQLPodReconciler{
+		Client: mgr.GetClient(),
+		Scheme: mgr.GetScheme(),
+	}).SetupWithManager(mgr); err != nil {
+		setupLog.Error(err, "unable to create controller", "controller", "PostgreSQLPod")
+		os.Exit(1)
+	}
+
 	if err = (&controller.PostgreSQLReconciler{
 		Client: mgr.GetClient(),
 		Scheme: mgr.GetScheme(),

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

@@ -14,7 +14,11 @@ spec:
     singular: postgresql
   scope: Namespaced
   versions:
-  - name: v1
+  - additionalPrinterColumns:
+    - jsonPath: .status.status
+      name: Status
+      type: string
+    name: v1
     schema:
       openAPIV3Schema:
         description: PostgreSQL is the Schema for the postgresqls API
@@ -34,13 +38,31 @@ spec:
           spec:
             description: PostgreSQLSpec defines the desired state of PostgreSQL
             properties:
-              foo:
-                description: Foo is an example field of PostgreSQL. Edit postgresql_types.go
-                  to remove/update
+              database:
+                description: Autogenerated name for default Database
                 type: string
+              password:
+                description: Password for authentication
+                type: string
+              user:
+                description: Username for authentication
+                type: string
+              version:
+                description: Version of Postgres to deploy. [16.0]
+                type: string
+            required:
+            - database
+            - password
+            - user
+            - version
             type: object
           status:
             description: PostgreSQLStatus defines the observed state of PostgreSQL
+            properties:
+              status:
+                type: string
+            required:
+            - status
             type: object
         type: object
     served: true

+ 8 - 0
config/rbac/role.yaml

@@ -4,6 +4,14 @@ kind: ClusterRole
 metadata:
   name: manager-role
 rules:
+- apiGroups:
+  - ""
+  resources:
+  - pods
+  verbs:
+  - create
+  - get
+  - watch
 - apiGroups:
   - database.iwanhae.kr
   resources:

+ 4 - 1
config/samples/database_v1_postgresql.yaml

@@ -9,4 +9,7 @@ metadata:
     app.kubernetes.io/created-by: nodb
   name: postgresql-sample
 spec:
-  # TODO(user): Add fields here
+  version: "16.0"
+  user: postgres
+  password: postgres
+  database: postgres

+ 7 - 6
go.mod

@@ -39,7 +39,7 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
-	github.com/pkg/errors v0.9.1 // indirect
+	github.com/pkg/errors v0.9.1
 	github.com/prometheus/client_golang v1.16.0 // indirect
 	github.com/prometheus/client_model v0.4.0 // indirect
 	github.com/prometheus/common v0.44.0 // indirect
@@ -48,11 +48,11 @@ require (
 	go.uber.org/multierr v1.11.0 // indirect
 	go.uber.org/zap v1.25.0 // indirect
 	golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e // indirect
-	golang.org/x/net v0.13.0 // indirect
+	golang.org/x/net v0.17.0 // indirect
 	golang.org/x/oauth2 v0.8.0 // indirect
-	golang.org/x/sys v0.11.0 // indirect
-	golang.org/x/term v0.10.0 // indirect
-	golang.org/x/text v0.11.0 // indirect
+	golang.org/x/sys v0.13.0 // indirect
+	golang.org/x/term v0.13.0 // indirect
+	golang.org/x/text v0.13.0 // indirect
 	golang.org/x/time v0.3.0 // indirect
 	golang.org/x/tools v0.9.3 // indirect
 	gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect
@@ -61,11 +61,12 @@ require (
 	gopkg.in/inf.v0 v0.9.1 // indirect
 	gopkg.in/yaml.v2 v2.4.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
-	k8s.io/api v0.28.0 // indirect
+	k8s.io/api v0.28.0
 	k8s.io/apiextensions-apiserver v0.28.0 // indirect
 	k8s.io/component-base v0.28.0 // indirect
 	k8s.io/klog/v2 v2.100.1 // indirect
 	k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
+	k8s.io/kubernetes v1.28.3
 	k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
 	sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
 	sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect

+ 12 - 10
go.sum

@@ -49,7 +49,7 @@ github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5y
 github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
 github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
 github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
-github.com/google/cel-go v0.16.0 h1:DG9YQ8nFCFXAs/FDDwBxmL1tpKNrdlGUM9U3537bX/Y=
+github.com/google/cel-go v0.16.1 h1:3hZfSNiAU3KOiNtxuFXVp5WFy4hf/Ly3Sa4/7F8SXNo=
 github.com/google/gnostic-models v0.6.8 h1:yo/ABAfM5IMRsS1VnXjTBvUb61tFIHozhlYvRgGre9I=
 github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYuF5OpfBSENuI8U=
 github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -154,7 +154,7 @@ go.uber.org/zap v1.25.0/go.mod h1:JIAUzQIH94IC4fOJQm7gMmBJP5k7wQfdcnYdPoEXJYk=
 golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
 golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
-golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e h1:+WEEuIdZHnUeJJmEUjyYC2gfUMj69yZXw17EnHg/otA=
 golang.org/x/exp v0.0.0-20220722155223-a9213eeb770e/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
 golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
@@ -169,8 +169,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
 golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
 golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
 golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.13.0 h1:Nvo8UFsZ8X3BhAC9699Z1j7XQ3rsZnUUm7jfBEk1ueY=
-golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
 golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
 golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -187,16 +187,16 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w
 golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
-golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
-golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
+golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
+golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
-golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
 golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
 golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -252,6 +252,8 @@ k8s.io/klog/v2 v2.100.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0=
 k8s.io/kms v0.28.0 h1:BwJhU9qPcJhHLUcQjtelOSjYti+1/caJLr+4jHbKzTA=
 k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 h1:LyMgNKD2P8Wn1iAwQU5OhxCKlKJy0sHc+PcDwFB24dQ=
 k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9/go.mod h1:wZK2AVp1uHCp4VamDVgBP2COHZjqD1T68Rf0CM3YjSM=
+k8s.io/kubernetes v1.28.3 h1:XTci6gzk+JR51UZuZQCFJ4CsyUkfivSjLI4O1P9z6LY=
+k8s.io/kubernetes v1.28.3/go.mod h1:NhAysZWvHtNcJFFHic87ofxQN7loylCQwg3ZvXVDbag=
 k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
 k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
 sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.1.2 h1:trsWhjU5jZrx6UvFu4WzQDrN7Pga4a7Qg+zcfcj64PA=

+ 54 - 2
internal/controller/postgresql_controller.go

@@ -19,12 +19,17 @@ package controller
 import (
 	"context"
 
+	apierrors "k8s.io/apimachinery/pkg/api/errors"
+	"k8s.io/apimachinery/pkg/api/resource"
 	"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"
+	"github.com/iwanhae/nodb/internal/templates"
+	"github.com/pkg/errors"
+	corev1 "k8s.io/api/core/v1"
 )
 
 // PostgreSQLReconciler reconciles a PostgreSQL object
@@ -36,6 +41,7 @@ type PostgreSQLReconciler struct {
 //+kubebuilder:rbac:groups=database.iwanhae.kr,resources=postgresqls,verbs=get;list;watch;create;update;patch;delete
 //+kubebuilder:rbac:groups=database.iwanhae.kr,resources=postgresqls/status,verbs=get;update;patch
 //+kubebuilder:rbac:groups=database.iwanhae.kr,resources=postgresqls/finalizers,verbs=update
+//+kubebuilder:rbac:groups="",resources=pods,verbs=get;create
 
 // Reconcile is part of the main kubernetes reconciliation loop which aims to
 // move the current state of the cluster closer to the desired state.
@@ -47,9 +53,55 @@ 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) {
-	_ = log.FromContext(ctx)
+	logger := log.FromContext(ctx)
 
-	// TODO(user): your logic here
+	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
+	}
+
+	logger.Info("reconcile", "namespace", obj.Namespace, "name", obj.Name)
+
+	// Pending: Creating Pod if not exists
+	// do not handle spec update
+
+	pod := corev1.Pod{}
+	if err := r.Client.Get(ctx, req.NamespacedName, &pod); err == nil {
+		// if found return
+		logger.Info("ignore already existing pod")
+		return ctrl.Result{}, nil
+	} else if !apierrors.IsNotFound(err) {
+		// weird error
+		return ctrl.Result{}, err
+	}
+
+	pod = templates.PostgreSQLPod(templates.PostgreSQLOpts{
+		Name:      obj.Name,
+		Namespace: obj.Namespace,
+		Tag:       obj.Spec.Version,
+
+		User:     obj.Spec.User,
+		Password: obj.Spec.Password,
+		Database: obj.Spec.Database,
+
+		Memory: resource.MustParse("1Gi"),
+		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")
+	}
+
+	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
 }

+ 99 - 0
internal/controller/postgresql_pod_controller.go

@@ -0,0 +1,99 @@
+/*
+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"
+	podutil "k8s.io/kubernetes/pkg/api/v1/pod"
+)
+
+// PostgreSQLReconciler reconciles a PostgreSQL object
+type PostgreSQLPodReconciler struct {
+	client.Client
+	Scheme *runtime.Scheme
+}
+
+//+kubebuilder:rbac:groups="",resources=pods,verbs=watch;get
+//+kubebuilder:rbac:groups=database.iwanhae.kr,resources=postgresqls/status,verbs=get;update;patch
+
+func (r *PostgreSQLPodReconciler) 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
+	}
+
+	// will update Status anyway
+	defer func() {
+		err = r.Status().Update(ctx, &obj)
+		if err != nil {
+			logger.Error(err, "failed to update status")
+		}
+	}()
+
+	// Find Pod
+	pod := corev1.Pod{}
+	if err := r.Get(ctx, req.NamespacedName, &pod); 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 pod.Labels[templates.LabelKeyType] != templates.LabelValuePostgreSQL {
+		// This pod is not for PostgreSQL
+		return ctrl.Result{}, nil
+	}
+
+	logger.Info("reconcile", "namespace", pod.Namespace, "name", pod.Name)
+
+	cond := podutil.GetPodReadyCondition(pod.Status)
+	if cond == nil {
+		obj.Status.Status = databasev1.Status_Initializing
+	} else if cond.Status == corev1.ConditionTrue {
+		obj.Status.Status = databasev1.Status_Ready
+	} else {
+		obj.Status.Status = databasev1.Status_NotReady
+	}
+
+	return ctrl.Result{}, nil
+}
+
+// SetupWithManager sets up the controller with the Manager.
+func (r *PostgreSQLPodReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	return ctrl.NewControllerManagedBy(mgr).
+		For(&corev1.Pod{}).
+		Complete(r)
+}

+ 98 - 0
internal/templates/postgresql.go

@@ -0,0 +1,98 @@
+package templates
+
+import (
+	"fmt"
+
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/util/intstr"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+const (
+	LabelKeyType         = "nodb.iwanhae.kr/type"
+	LabelValuePostgreSQL = "postgresql"
+)
+
+type PostgreSQLOpts struct {
+	Name      string
+	Namespace string
+
+	Tag string
+
+	User     string
+	Password string
+	Database string
+
+	Memory resource.Quantity
+
+	Owner metav1.Object
+}
+
+func PostgreSQLPod(opts PostgreSQLOpts) corev1.Pod {
+	pod := corev1.Pod{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      opts.Name,
+			Namespace: opts.Namespace,
+			Labels: map[string]string{
+				LabelKeyType: LabelValuePostgreSQL,
+			},
+		},
+		Spec: corev1.PodSpec{
+			RestartPolicy: corev1.RestartPolicyAlways,
+			Containers: []corev1.Container{
+				{
+					Name:  "postgres",
+					Image: fmt.Sprintf("postgres:%s", opts.Tag),
+					Ports: []corev1.ContainerPort{
+						{Name: "postgres", ContainerPort: 5432, Protocol: corev1.ProtocolTCP},
+					},
+					Env: []corev1.EnvVar{
+						{Name: "POSTGRES_USER", Value: opts.User},
+						{Name: "POSTGRES_PASSWORD", Value: opts.Password},
+						{Name: "POSTGRES_DB", Value: opts.Database},
+						{Name: "PGDATA", Value: "/pgdata"},
+					},
+					Resources: corev1.ResourceRequirements{
+						Limits: corev1.ResourceList{
+							corev1.ResourceMemory: opts.Memory,
+						},
+					},
+					StartupProbe: &corev1.Probe{
+						ProbeHandler: corev1.ProbeHandler{
+							TCPSocket: &corev1.TCPSocketAction{Port: intstr.FromInt(5432)},
+						},
+						InitialDelaySeconds: 3,
+						TimeoutSeconds:      5,
+						FailureThreshold:    5,
+						SuccessThreshold:    1,
+					},
+					ReadinessProbe: &corev1.Probe{
+						ProbeHandler: corev1.ProbeHandler{
+							TCPSocket: &corev1.TCPSocketAction{Port: intstr.FromInt(5432)},
+						},
+						InitialDelaySeconds: 3,
+						TimeoutSeconds:      5,
+						FailureThreshold:    5,
+						SuccessThreshold:    1,
+					},
+					VolumeMounts: []corev1.VolumeMount{
+						{
+							Name:      "pgdata",
+							MountPath: "/pgdata",
+						},
+					},
+				},
+			},
+			Volumes: []corev1.Volume{
+				{
+					Name:         "pgdata",
+					VolumeSource: corev1.VolumeSource{EmptyDir: &corev1.EmptyDirVolumeSource{}},
+				},
+			},
+		},
+	}
+	controllerutil.SetOwnerReference(opts.Owner, &pod, scheme)
+	return pod
+}

+ 9 - 0
internal/templates/scheme.go

@@ -0,0 +1,9 @@
+package templates
+
+import "k8s.io/apimachinery/pkg/runtime"
+
+var scheme *runtime.Scheme
+
+func SetScheme(s *runtime.Scheme) {
+	scheme = s
+}