Skip to content

Commit

Permalink
Merge pull request #35 from vshn/postgresql
Browse files Browse the repository at this point in the history
Define API scheme for managed PostgreSQL DBaaS
  • Loading branch information
ccremer authored Oct 19, 2022
2 parents 18b3ce5 + 0b45b11 commit 2c3cdd2
Show file tree
Hide file tree
Showing 11 changed files with 1,168 additions and 28 deletions.
140 changes: 140 additions & 0 deletions apis/exoscale/v1/dbaas_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
package v1

import (
"fmt"
"regexp"
"strconv"

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
exoscaleoapi "github.com/exoscale/egoscale/v2/oapi"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
)

type DBaaSParameters struct {
// TerminationProtection protects against termination and powering off.
TerminationProtection bool `json:"terminationProtection,omitempty"`
// Version is the (major) version identifier for the instance.
Version string `json:"version,omitempty"`
// Size contains the service capacity settings.
Size SizeSpec `json:"size,omitempty"`

IPFilter IPFilter `json:"ipFilter,omitempty"`
}

// NodeState describes the state of a service node.
type NodeState struct {
// Name of the service node
Name string `json:"name,omitempty"`
// Role of this node.
Role exoscaleoapi.DbaasNodeStateRole `json:"role,omitempty"`
// State of the service node.
State exoscaleoapi.DbaasNodeStateState `json:"state,omitempty"`
}

// Notification contains a service message.
type Notification struct {
// Level of the notification.
Level exoscaleoapi.DbaasServiceNotificationLevel `json:"level,omitempty"`
// Message contains the notification.
Message string `json:"message,omitempty"`
// Type of the notification.
Type exoscaleoapi.DbaasServiceNotificationType `json:"type,omitempty"`
// Metadata contains additional data.
Metadata runtime.RawExtension `json:"metadata,omitempty"`
}

// BackupSpec contains settings to control the backups of an instance.
type BackupSpec struct {
// TimeOfDay for doing daily backups, in UTC.
// Format: "hh:mm:ss".
TimeOfDay TimeOfDay `json:"timeOfDay,omitempty"`
}

// MaintenanceSpec contains settings to control the maintenance of an instance.
type MaintenanceSpec struct {
// +kubebuilder:validation:Enum=monday;tuesday;wednesday;thursday;friday;saturday;sunday;never

// DayOfWeek specifies at which weekday the maintenance is held place.
// Allowed values are [monday, tuesday, wednesday, thursday, friday, saturday, sunday, never]
DayOfWeek exoscaleoapi.DbaasServiceMaintenanceDow `json:"dayOfWeek,omitempty"`

// TimeOfDay for installing updates in UTC.
// Format: "hh:mm:ss".
TimeOfDay TimeOfDay `json:"timeOfDay,omitempty"`
}

var timeOfDayRegex = regexp.MustCompile("^([0-1]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$")

// +kubebuilder:validation:Pattern="^([0-1]?[0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9])$"

// TimeOfDay contains a time in the 24hr clock.
// Format: "hh:mm:ss".
type TimeOfDay string

// String implements fmt.Stringer.
func (t TimeOfDay) String() string {
return string(t)
}

// Parse returns the hour and minute of the string representation.
// Returns errors if the format is invalid.
func (t TimeOfDay) Parse() (hour, minute, second int64, err error) {
if t.String() == "" {
return -1, -1, -1, fmt.Errorf("time cannot be empty")
}
arr := timeOfDayRegex.FindStringSubmatch(t.String())
if len(arr) < 3 {
return -1, -1, -1, fmt.Errorf("invalid format for time of day (hh:mm:ss): %s", t)
}
parts := []int64{0, 0, 0}
for i, part := range arr[1:] {
parsed, err := strconv.ParseInt(part, 10, 64)
if err != nil && part != "" {
return -1, -1, -1, fmt.Errorf("invalid time given for time of day: %w", err)
}
parts[i] = parsed
}
return parts[0], parts[1], parts[2], nil
}

// Rebuilding returns a Ready condition where the service is rebuilding.
func Rebuilding() xpv1.Condition {
return xpv1.Condition{
Type: xpv1.TypeReady,
Status: corev1.ConditionFalse,
Reason: "Rebuilding",
Message: "The service is being provisioned",
LastTransitionTime: metav1.Now(),
}
}

// PoweredOff returns a Ready condition where the service is powered off.
func PoweredOff() xpv1.Condition {
return xpv1.Condition{
Type: xpv1.TypeReady,
Status: corev1.ConditionFalse,
Reason: "PoweredOff",
Message: "The service is powered off",
LastTransitionTime: metav1.Now(),
}
}

// Rebalancing returns a Ready condition where the service is rebalancing.
func Rebalancing() xpv1.Condition {
return xpv1.Condition{
Type: xpv1.TypeReady,
Status: corev1.ConditionFalse,
Reason: "Rebalancing",
Message: "The service is being rebalanced",
LastTransitionTime: metav1.Now(),
}
}

// Running returns a Ready condition where the service is running.
func Running() xpv1.Condition {
c := xpv1.Available()
c.Message = "The service is running"
return c
}
51 changes: 51 additions & 0 deletions apis/exoscale/v1/dbaas_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package v1

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestTimeOfDay_Parse(t *testing.T) {
tests := []struct {
givenInput string
expectedMinute int64
expectedHour int64
expectedSecond int64
expectedError string
}{
{givenInput: "00:00:00", expectedHour: 0, expectedMinute: 0, expectedSecond: 0},
{givenInput: "23:59:00", expectedHour: 23, expectedMinute: 59, expectedSecond: 0},
{givenInput: "01:01:01", expectedHour: 1, expectedMinute: 1, expectedSecond: 1},
{givenInput: "1:59:59", expectedHour: 1, expectedMinute: 59, expectedSecond: 59},
{givenInput: "19:59:01", expectedHour: 19, expectedMinute: 59, expectedSecond: 1},
{givenInput: "4:59:01", expectedHour: 4, expectedMinute: 59, expectedSecond: 1},
{givenInput: "04:59:01", expectedHour: 4, expectedMinute: 59, expectedSecond: 1},
{givenInput: "9:01:01", expectedHour: 9, expectedMinute: 1, expectedSecond: 1},
{givenInput: "", expectedError: "time cannot be empty"},
{givenInput: "invalid", expectedError: "invalid format for time of day (hh:mm:ss): invalid"},
{givenInput: "🕗", expectedError: "invalid format for time of day (hh:mm:ss): 🕗"},
{givenInput: "-1:1", expectedError: "invalid format for time of day (hh:mm:ss): -1:1"},
{givenInput: "24:01:30", expectedError: "invalid format for time of day (hh:mm:ss): 24:01:30"},
{givenInput: "23:60:00", expectedError: "invalid format for time of day (hh:mm:ss): 23:60:00"},
{givenInput: "foo:01:02", expectedError: "invalid format for time of day (hh:mm:ss): foo:01:02"},
{givenInput: "01:bar:02", expectedError: "invalid format for time of day (hh:mm:ss): 01:bar:02"},
}
for _, tc := range tests {
t.Run(tc.givenInput, func(t *testing.T) {
hour, minute, second, err := TimeOfDay(tc.givenInput).Parse()
if tc.expectedError != "" {
assert.EqualError(t, err, tc.expectedError)
assert.Equal(t, int64(-1), hour)
assert.Equal(t, int64(-1), minute)
assert.Equal(t, int64(-1), second)

} else {
assert.NoError(t, err)
assert.Equal(t, tc.expectedHour, hour)
assert.Equal(t, tc.expectedMinute, minute)
assert.Equal(t, tc.expectedSecond, second)
}
})
}
}
119 changes: 119 additions & 0 deletions apis/exoscale/v1/postgresql_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package v1

import (
"reflect"

xpv1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
"github.com/crossplane/crossplane-runtime/pkg/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
)

// PostgreSQLParameters are the configurable fields of a PostgreSQL.
type PostgreSQLParameters struct {
Maintenance MaintenanceSpec `json:"maintenance,omitempty"`
Backup BackupSpec `json:"backup,omitempty"`

// +kubebuilder:validation:Enum=ch-gva-2;ch-dk-2;de-fra-1;de-muc-1;at-vie-1;bg-sof-1
// +kubebuilder:validation:Required

// Zone is the datacenter identifier in which the instance runs in.
Zone string `json:"zone"`

DBaaSParameters `json:",inline"`

// PGSettings contains additional PostgreSQL settings.
PGSettings runtime.RawExtension `json:"pgSettings,omitempty"`
}

// SizeSpec contains settings to control the sizing of a service.
type SizeSpec struct {
Plan string `json:"plan,omitempty"`
}

// IPFilter is a list of allowed IPv4 CIDR ranges that can access the service.
// If no IP Filter is set, you may not be able to reach the service.
// A value of `0.0.0.0/0` will open the service to all addresses on the public internet.
type IPFilter []string

// PostgreSQLSpec defines the desired state of a PostgreSQL.
type PostgreSQLSpec struct {
xpv1.ResourceSpec `json:",inline"`
ForProvider PostgreSQLParameters `json:"forProvider"`
}

// PostgreSQLObservation are the observable fields of a PostgreSQL.
type PostgreSQLObservation struct {
DBaaSParameters `json:",inline"`
Maintenance MaintenanceSpec `json:"maintenance,omitempty"`
Backup BackupSpec `json:"backup,omitempty"`
NoteStates []NodeState `json:"noteStates,omitempty"`
PGSettings runtime.RawExtension `json:"pgSettings,omitempty"`
}

// PostgreSQLStatus represents the observed state of a PostgreSQL.
type PostgreSQLStatus struct {
xpv1.ResourceStatus `json:",inline"`
AtProvider PostgreSQLObservation `json:"atProvider,omitempty"`
}

// +kubebuilder:object:root=true
// +kubebuilder:printcolumn:name="State",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].reason"
// +kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type=='Ready')].status"
// +kubebuilder:printcolumn:name="Synced",type="string",JSONPath=".status.conditions[?(@.type=='Synced')].status"
// +kubebuilder:printcolumn:name="External Name",type="string",JSONPath=".metadata.annotations.crossplane\\.io/external-name"
// +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp"
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster,categories={crossplane,exoscale}
// +kubebuilder:webhook:verbs=create;update,path=/validate-exoscale-crossplane-io-v1-postgresql,mutating=false,failurePolicy=fail,groups=exoscale.crossplane.io,resources=postgresqls,versions=v1,name=postgresqls.exoscale.crossplane.io,sideEffects=None,admissionReviewVersions=v1

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

Spec PostgreSQLSpec `json:"spec"`
Status PostgreSQLStatus `json:"status,omitempty"`
}

// GetProviderConfigName returns the name of the ProviderConfig.
// Returns empty string if reference not given.
func (in *PostgreSQL) GetProviderConfigName() string {
if ref := in.GetProviderConfigReference(); ref != nil {
return ref.Name
}
return ""
}

// GetInstanceName returns the external name of the instance in the following precedence:
//
// .metadata.annotations."crossplane.io/external-name"
// .metadata.name
func (in *PostgreSQL) GetInstanceName() string {
if name := meta.GetExternalName(in); name != "" {
return name
}
return in.Name
}

// +kubebuilder:object:root=true

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

// PostgreSQL type metadata.
var (
PostgreSQLKind = reflect.TypeOf(PostgreSQL{}).Name()
PostgreSQLGroupKind = schema.GroupKind{Group: Group, Kind: PostgreSQLKind}.String()
PostgreSQLKindAPIVersion = PostgreSQLKind + "." + SchemeGroupVersion.String()
PostgreSQLGroupVersionKind = SchemeGroupVersion.WithKind(PostgreSQLKind)
)

func init() {
SchemeBuilder.Register(&PostgreSQL{}, &PostgreSQLList{})
}
Loading

0 comments on commit 2c3cdd2

Please sign in to comment.