Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add database support #55

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion PROJECT
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,16 @@ resources:
webhooks:
validation: true
webhookVersion: v1
version: "3"
- api:
crdVersion: v1
namespaced: true
controller: true
domain: digitalocean.com
group: databases
kind: Database
path: github.com/digitalocean/do-operator/api/v1alpha1
version: v1alpha1
webhooks:
conversion: true
webhookVersion: v1
version: "3"
67 changes: 67 additions & 0 deletions api/v1alpha1/database_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
Copyright 2022 DigitalOcean.

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 v1alpha1

import (
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// DatabaseSpec defines the desired state of Database
type DatabaseSpec struct {
// Cluster is a reference to the DatabaseCluster or DatabaseClusterReference
// that represents the database cluster in which the database will be created.
Cluster corev1.TypedLocalObjectReference `json:"databaseCluster"`
// name is the name for the database.
Name string `json:"name"`
}

// DatabaseStatus defines the observed state of Database
type DatabaseStatus struct {
// ClusterUUID is the UUID of the cluster this database is in. We keep this in
// the status so that we can manage the user even if the referenced Cluster
// CR is deleted.
ClusterUUID string `json:"clusterUUID,omitempty"`
Name string `json:"name,omitempty"`
}

//+kubebuilder:object:root=true
//+kubebuilder:subresource:status
//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
//+kubebuilder:printcolumn:name="Name",type=string,JSONPath=`.spec.name`

// Database is the Schema for the databases API
type Database struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`

Spec DatabaseSpec `json:"spec,omitempty"`
Status DatabaseStatus `json:"status,omitempty"`
}

//+kubebuilder:object:root=true

// DatabaseList contains a list of Database
type DatabaseList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Database `json:"items"`
}

func init() {
SchemeBuilder.Register(&Database{}, &DatabaseList{})
}
138 changes: 138 additions & 0 deletions api/v1alpha1/database_webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
Copyright 2022 DigitalOcean.

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 v1alpha1

import (
"context"
"fmt"
"net/http"
"strings"

"sigs.k8s.io/controller-runtime/pkg/webhook/admission"

"github.com/digitalocean/godo"
"github.com/google/go-cmp/cmp"
kerrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/utils/pointer"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
)

// log is for logging in this package.
var databaselog = logf.Log.WithName("database-resource")

func (r *Database) SetupWebhookWithManager(mgr ctrl.Manager, godoClient *godo.Client) error {
initGlobalGodoClient(godoClient)
initGlobalK8sClient(mgr.GetClient())

return ctrl.NewWebhookManagedBy(mgr).
For(r).
Complete()
}

//+kubebuilder:webhook:path=/validate-databases-digitalocean-com-v1alpha1-database,mutating=false,failurePolicy=fail,sideEffects=None,groups=databases.digitalocean.com,resources=databases,verbs=create;update,versions=v1alpha1,name=vdatabase.kb.io,admissionReviewVersions=v1

var _ webhook.Validator = &Database{}

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *Database) ValidateCreate() (warnings admission.Warnings, err error) {
databaselog.Info("validate create", "name", r.Name)
ctx := context.TODO()

clusterPath := field.NewPath("spec").Child("cluster")

clusterAPIGroup := pointer.StringDeref(r.Spec.Cluster.APIGroup, "")
if clusterAPIGroup != GroupVersion.Group {
return warnings, field.Invalid(clusterPath.Child("apiGroup"), clusterAPIGroup, "apiGroup must be "+GroupVersion.Group)
}

var (
clusterNN = types.NamespacedName{
Namespace: r.Namespace,
Name: r.Spec.Cluster.Name,
}
clusterKind = r.Spec.Cluster.Kind
clusterUUID string
)

switch strings.ToLower(clusterKind) {
case strings.ToLower(DatabaseClusterKind):
var cluster DatabaseCluster
if err := webhookClient.Get(ctx, clusterNN, &cluster); err != nil {
if kerrors.IsNotFound(err) {
return warnings, field.NotFound(clusterPath, clusterNN)
}
return warnings, fmt.Errorf("failed to fetch DatabaseCluster %s: %s", clusterNN, err)
}
clusterUUID = cluster.Status.UUID
case strings.ToLower(DatabaseClusterReferenceKind):
var clusterRef DatabaseClusterReference
if err := webhookClient.Get(ctx, clusterNN, &clusterRef); err != nil {
if kerrors.IsNotFound(err) {
return warnings, field.NotFound(clusterPath, clusterNN)
}
return warnings, fmt.Errorf("failed to fetch DatabaseClusterReference %s: %s", clusterNN, err)
}
clusterUUID = clusterRef.Spec.UUID
default:
return warnings, field.TypeInvalid(
clusterPath.Child("kind"),
clusterKind,
"kind must be DatabaseCluster or DatabaseClusterReference",
)
}

_, resp, err := godoClient.Databases.GetDB(ctx, clusterUUID, r.Spec.Name)
if err != nil && resp.StatusCode != http.StatusNotFound {
return warnings, fmt.Errorf("failed to look up database: %v", err)
}
if err == nil {
return warnings, field.Duplicate(field.NewPath("spec").Child("name"), r.Spec.Name)
}

return warnings, nil
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *Database) ValidateUpdate(old runtime.Object) (warnings admission.Warnings, err error) {
databaselog.Info("validate update", "name", r.Name)

oldDatabase, ok := old.(*Database)
if !ok {
return warnings, fmt.Errorf("old is unexpected type %T", old)
}
namePath := field.NewPath("spec").Child("name")
if r.Spec.Name != oldDatabase.Spec.Name {
return warnings, field.Forbidden(namePath, "name is immutable")
}
clusterPath := field.NewPath("spec").Child("cluster")
if !cmp.Equal(r.Spec.Cluster, oldDatabase.Spec.Cluster) {
return warnings, field.Forbidden(clusterPath, "cluster is immutable")
}

return warnings, nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *Database) ValidateDelete() (warnings admission.Warnings, err error) {
databaselog.Info("validate delete", "name", r.Name)
return warnings, nil
}
88 changes: 88 additions & 0 deletions config/crd/bases/databases.digitalocean.com_databases.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.9.0
creationTimestamp: null
name: databases.databases.digitalocean.com
spec:
group: databases.digitalocean.com
names:
kind: Database
listKind: DatabaseList
plural: databases
singular: database
scope: Namespaced
versions:
- additionalPrinterColumns:
- jsonPath: .metadata.creationTimestamp
name: Age
type: date
- jsonPath: .spec.name
name: Name
type: string
name: v1alpha1
schema:
openAPIV3Schema:
description: Database is the Schema for the databases 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: DatabaseSpec defines the desired state of Database
properties:
databaseCluster:
description: Cluster is a reference to the DatabaseCluster or DatabaseClusterReference
that represents the database cluster in which the database will
be created.
properties:
apiGroup:
description: APIGroup is the group for the resource being referenced.
If APIGroup is not specified, the specified Kind must be in
the core API group. For any other third-party types, APIGroup
is required.
type: string
kind:
description: Kind is the type of resource being referenced
type: string
name:
description: Name is the name of resource being referenced
type: string
required:
- kind
- name
type: object
name:
description: name is the name for the database.
type: string
required:
- databaseCluster
- name
type: object
status:
description: DatabaseStatus defines the observed state of Database
properties:
clusterUUID:
description: ClusterUUID is the UUID of the cluster this database
is in. We keep this in the status so that we can manage the user
even if the referenced Cluster CR is deleted.
type: string
name:
type: string
type: object
type: object
served: true
storage: true
subresources:
status: {}
3 changes: 3 additions & 0 deletions config/crd/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ resources:
- bases/databases.digitalocean.com_databaseclusterreferences.yaml
- bases/databases.digitalocean.com_databaseusers.yaml
- bases/databases.digitalocean.com_databaseuserreferences.yaml
- bases/databases.digitalocean.com_databases.yaml
#+kubebuilder:scaffold:crdkustomizeresource

patchesStrategicMerge:
Expand All @@ -15,6 +16,7 @@ patchesStrategicMerge:
- patches/webhook_in_databaseclusterreferences.yaml
- patches/webhook_in_databaseusers.yaml
- patches/webhook_in_databaseuserreferences.yaml
- patches/webhook_in_databases.yaml
#+kubebuilder:scaffold:crdkustomizewebhookpatch

# [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix.
Expand All @@ -23,6 +25,7 @@ patchesStrategicMerge:
- patches/cainjection_in_databaseclusterreferences.yaml
- patches/cainjection_in_databaseusers.yaml
- patches/cainjection_in_databaseuserreferences.yaml
- patches/cainjection_in_databases.yaml
#+kubebuilder:scaffold:crdkustomizecainjectionpatch

# the following config is for teaching kustomize how to do kustomization for CRDs.
Expand Down
7 changes: 7 additions & 0 deletions config/crd/patches/cainjection_in_databases.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# The following patch adds a directive for certmanager to inject CA into the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)
name: databases.databases.digitalocean.com
16 changes: 16 additions & 0 deletions config/crd/patches/webhook_in_databases.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# The following patch enables a conversion webhook for the CRD
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: databases.databases.digitalocean.com
spec:
conversion:
strategy: Webhook
webhook:
clientConfig:
service:
namespace: system
name: webhook-service
path: /convert
conversionReviewVersions:
- v1
24 changes: 24 additions & 0 deletions config/rbac/database_editor_role.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# permissions for end s to edit databases.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: database-editor-role
rules:
- apiGroups:
- databases.digitalocean.com
resources:
- databases
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- databases.digitalocean.com
resources:
- databases/status
verbs:
- get
Loading