Skip to content

Commit

Permalink
Merge pull request #103 from ninech/marco/postgres
Browse files Browse the repository at this point in the history
feat(postgres): Add resource
  • Loading branch information
9marco authored Jun 6, 2024
2 parents fcd8077 + f2fe4ac commit 8ec00aa
Show file tree
Hide file tree
Showing 24 changed files with 745 additions and 27 deletions.
1 change: 1 addition & 0 deletions create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type Cmd struct {
Config configCmd `cmd:"" group:"deplo.io" name:"config" help:"Create a new deplo.io Project Configuration. (Beta - requires access)"`
Application applicationCmd `cmd:"" group:"deplo.io" name:"application" aliases:"app" help:"Create a new deplo.io Application. (Beta - requires access)"`
MySQL mySQLCmd `cmd:"" group:"storage.nine.ch" name:"mysql" help:"Create a new MySQL instance."`
Postgres postgresCmd `cmd:"" group:"storage.nine.ch" name:"postgres" help:"Create a new PostgreSQL instance."`
KeyValueStore keyValueStoreCmd `cmd:"" group:"storage.nine.ch" name:"keyvaluestore" aliases:"kvs" help:"Create a new KeyValueStore instance"`
CloudVirtualMachine cloudVMCmd `cmd:"" group:"infrastructure.nine.ch" name:"cloudvirtualmachine" aliases:"cloudvm" help:"Create a new CloudVM."`
}
Expand Down
2 changes: 1 addition & 1 deletion create/keyvaluestore.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type keyValueStoreCmd struct {
Location string `default:"nine-es34" help:"Location where the KeyValueStore instance is created."`
MemorySize string `help:"MemorySize configures KeyValueStore to use a specified amount of memory for the data set." placeholder:"1Gi"`
MaxMemoryPolicy storage.KeyValueStoreMaxMemoryPolicy `help:"MaxMemoryPolicy specifies the exact behavior KeyValueStore follows when the maxmemory limit is reached." placeholder:"allkeys-lru"`
AllowedCidrs []storage.IPv4CIDR `help:"AllowedCIDRs specify the allowed IP addresses, connecting to the instance." placeholder:"0.0.0.0/0"`
AllowedCidrs []meta.IPv4CIDR `help:"AllowedCIDRs specify the allowed IP addresses, connecting to the instance." placeholder:"0.0.0.0/0"`
Wait bool `default:"true" help:"Wait until KeyValueStore is created."`
WaitTimeout time.Duration `default:"600s" help:"Duration to wait for KeyValueStore getting ready. Only relevant if --wait is set."`
}
Expand Down
5 changes: 3 additions & 2 deletions create/keyvaluestore_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"testing"
"time"

meta "github.com/ninech/apis/meta/v1alpha1"
storage "github.com/ninech/apis/storage/v1alpha1"
"github.com/ninech/nctl/api"
"k8s.io/apimachinery/pkg/api/resource"
Expand Down Expand Up @@ -35,8 +36,8 @@ func TestKeyValueStore(t *testing.T) {
},
{
"allowedCIDRs",
keyValueStoreCmd{AllowedCidrs: []storage.IPv4CIDR{storage.IPv4CIDR("0.0.0.0/0")}},
storage.KeyValueStoreParameters{AllowedCIDRs: []storage.IPv4CIDR{storage.IPv4CIDR("0.0.0.0/0")}},
keyValueStoreCmd{AllowedCidrs: []meta.IPv4CIDR{meta.IPv4CIDR("0.0.0.0/0")}},
storage.KeyValueStoreParameters{AllowedCIDRs: []meta.IPv4CIDR{meta.IPv4CIDR("0.0.0.0/0")}},
false,
},
{
Expand Down
6 changes: 3 additions & 3 deletions create/mysql.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type mySQLCmd struct {
Name string `arg:"" default:"" help:"Name of the MySQL instance. A random name is generated if omitted."`
Location string `placeholder:"${mysql_location_default}" help:"Location where the MySQL instance is created. Available locations are: ${mysql_location_options}"`
MachineType infra.MachineType `placeholder:"${mysql_machine_default}" help:"Defines the sizing for a particular MySQL instance. Available types: ${mysql_machine_types}"`
AllowedCidrs []storage.IPv4CIDR `placeholder:"0.0.0.0/0" help:"Specifies the IP addresses allowed to connect to the instance." `
AllowedCidrs []meta.IPv4CIDR `placeholder:"0.0.0.0/0" help:"Specifies the IP addresses allowed to connect to the instance." `
SSHKeys []storage.SSHKey `help:"Contains a list of SSH public keys, allowed to connect to the db server, in order to up-/download and directly restore database backups."`
SSHKeysFile string `help:"Path to a file containing a list of SSH public keys (see above), separated by newlines."`
SQLMode *[]storage.MySQLMode `placeholder:"\"MODE1, MODE2, ...\"" help:"Configures the sql_mode setting. Modes affect the SQL syntax MySQL supports and the data validation checks it performs. Defaults to: ${mysql_mode}"`
Expand Down Expand Up @@ -89,8 +89,8 @@ func (cmd *mySQLCmd) newMySQL(namespace string) *storage.MySQL {
ForProvider: storage.MySQLParameters{
Location: meta.LocationName(cmd.Location),
MachineType: cmd.MachineType,
AllowedCIDRs: []storage.IPv4CIDR{}, // avoid missing parameter error
SSHKeys: []storage.SSHKey{}, // avoid missing parameter error
AllowedCIDRs: []meta.IPv4CIDR{}, // avoid missing parameter error
SSHKeys: []storage.SSHKey{}, // avoid missing parameter error
SQLMode: cmd.SQLMode,
CharacterSet: storage.MySQLCharacterSet{
Name: cmd.CharacterSetName,
Expand Down
7 changes: 4 additions & 3 deletions create/mysql_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"time"

infra "github.com/ninech/apis/infrastructure/v1alpha1"
meta "github.com/ninech/apis/meta/v1alpha1"
storage "github.com/ninech/apis/storage/v1alpha1"
"github.com/ninech/nctl/api"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
Expand Down Expand Up @@ -57,8 +58,8 @@ func TestMySQL(t *testing.T) {
},
{
name: "allowedCIDRs",
create: mySQLCmd{AllowedCidrs: []storage.IPv4CIDR{storage.IPv4CIDR("0.0.0.0/0")}},
want: storage.MySQLParameters{AllowedCIDRs: []storage.IPv4CIDR{storage.IPv4CIDR("0.0.0.0/0")}},
create: mySQLCmd{AllowedCidrs: []meta.IPv4CIDR{meta.IPv4CIDR("0.0.0.0/0")}},
want: storage.MySQLParameters{AllowedCIDRs: []meta.IPv4CIDR{meta.IPv4CIDR("0.0.0.0/0")}},
},
{
name: "characterSet",
Expand Down Expand Up @@ -118,7 +119,7 @@ func TestMySQL(t *testing.T) {

// we set defaults for the slices
if tt.want.AllowedCIDRs == nil {
tt.want.AllowedCIDRs = []storage.IPv4CIDR{}
tt.want.AllowedCIDRs = []meta.IPv4CIDR{}
}
if tt.want.SSHKeys == nil {
tt.want.SSHKeys = []storage.SSHKey{}
Expand Down
123 changes: 123 additions & 0 deletions create/postgres.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package create

import (
"context"
"fmt"
"strings"
"time"

metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"

"github.com/alecthomas/kong"
runtimev1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
infra "github.com/ninech/apis/infrastructure/v1alpha1"
meta "github.com/ninech/apis/meta/v1alpha1"
storage "github.com/ninech/apis/storage/v1alpha1"

"github.com/ninech/nctl/api"
"github.com/ninech/nctl/internal/file"
)

type postgresCmd struct {
Name string `arg:"" default:"" help:"Name of the PostgreSQL instance. A random name is generated if omitted."`
Location string `placeholder:"${postgres_location_default}" help:"Location where the PostgreSQL instance is created. Available locations are: ${postgres_location_options}"`
MachineType infra.MachineType `placeholder:"${postgres_machine_default}" help:"Defines the sizing for a particular PostgreSQL instance. Available types: ${postgres_machine_types}"`
AllowedCidrs []meta.IPv4CIDR `placeholder:"0.0.0.0/0" help:"Specifies the IP addresses allowed to connect to the instance." `
SSHKeys []storage.SSHKey `help:"Contains a list of SSH public keys, allowed to connect to the db server, in order to up-/download and directly restore database backups."`
SSHKeysFile string `help:"Path to a file containing a list of SSH public keys (see above), separated by newlines."`
PostgresVersion storage.PostgresVersion `placeholder:"${postgres_version_default}" help:"Release version with which the PostgreSQL instance is created"`
KeepDailyBackups *int `placeholder:"${postgres_backup_retention_days}" help:"Number of daily database backups to keep. Note that setting this to 0, backup will be disabled and existing dumps deleted immediately."`
Wait bool `default:"true" help:"Wait until PostgreSQL instance is created."`
WaitTimeout time.Duration `default:"25m" help:"Duration to wait for PostgreSQL getting ready. Only relevant if --wait is set."`
}

func (cmd *postgresCmd) Run(ctx context.Context, client *api.Client) error {
sshkeys, err := file.ReadSSHKeys(cmd.SSHKeysFile)
if err != nil {
return fmt.Errorf("error when reading SSH keys file: %w", err)
}
cmd.SSHKeys = append(cmd.SSHKeys, sshkeys...)

fmt.Printf("Creating new postgres. This might take some time (waiting up to %s).\n", cmd.WaitTimeout)
postgres := cmd.newPostgres(client.Project)

c := newCreator(client, postgres, "postgres")
ctx, cancel := context.WithTimeout(ctx, cmd.WaitTimeout)
defer cancel()

if err := c.createResource(ctx); err != nil {
return err
}

if !cmd.Wait {
return nil
}

return c.wait(ctx, waitStage{
objectList: &storage.PostgresList{},
onResult: func(event watch.Event) (bool, error) {
if c, ok := event.Object.(*storage.Postgres); ok {
return isAvailable(c), nil
}
return false, nil
},
},
)
}

func (cmd *postgresCmd) newPostgres(namespace string) *storage.Postgres {
name := getName(cmd.Name)

postgres := &storage.Postgres{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: storage.PostgresSpec{
ResourceSpec: runtimev1.ResourceSpec{
WriteConnectionSecretToReference: &runtimev1.SecretReference{
Name: "postgres-" + name,
Namespace: namespace,
},
},
ForProvider: storage.PostgresParameters{
Location: meta.LocationName(cmd.Location),
MachineType: cmd.MachineType,
AllowedCIDRs: []meta.IPv4CIDR{}, // avoid missing parameter error
SSHKeys: []storage.SSHKey{}, // avoid missing parameter error
Version: cmd.PostgresVersion,
KeepDailyBackups: cmd.KeepDailyBackups,
},
},
}

if cmd.AllowedCidrs != nil {
postgres.Spec.ForProvider.AllowedCIDRs = cmd.AllowedCidrs
}
if cmd.SSHKeys != nil {
postgres.Spec.ForProvider.SSHKeys = cmd.SSHKeys
}

return postgres
}

// ApplicationKongVars returns all variables which are used in the application
// create command
func PostgresKongVars() kong.Vars {
vmTypes := make([]string, len(infra.MachineTypes))
for i, machineType := range infra.MachineTypes {
vmTypes[i] = string(machineType)
}

result := make(kong.Vars)
result["postgres_machine_types"] = strings.Join(vmTypes, ", ")
result["postgres_machine_default"] = string(infra.MachineTypes[0])
result["postgres_location_options"] = strings.Join(storage.PostgresLocationOptions, ", ")
result["postgres_location_default"] = string(storage.PostgresLocationDefault)
result["postgres_version_default"] = string(storage.PostgresVersionDefault)
result["postgres_user"] = storage.PostgresUser
result["postgres_backup_retention_days"] = fmt.Sprintf("%d", storage.PostgresBackupRetentionDaysDefault)

return result
}
113 changes: 113 additions & 0 deletions create/postgres_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package create

import (
"context"
"errors"
"reflect"
"testing"
"time"

infra "github.com/ninech/apis/infrastructure/v1alpha1"
meta "github.com/ninech/apis/meta/v1alpha1"
storage "github.com/ninech/apis/storage/v1alpha1"
"github.com/ninech/nctl/api"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/ptr"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
)

func TestPostgres(t *testing.T) {
tests := []struct {
name string
create postgresCmd
want storage.PostgresParameters
wantErr bool
interceptorFuncs *interceptor.Funcs
}{
{
name: "simple",
create: postgresCmd{},
want: storage.PostgresParameters{},
},
{
name: "simpleErrorOnCreation",
create: postgresCmd{},
wantErr: true,
interceptorFuncs: &interceptor.Funcs{
Create: func(_ context.Context, _ client.WithWatch, _ client.Object, _ ...client.CreateOption) error {
return errors.New("error on creation")
},
},
},
{
name: "machineType",
create: postgresCmd{MachineType: infra.MachineType("nine-standard-1")},
want: storage.PostgresParameters{MachineType: infra.MachineType("nine-standard-1")},
},
{
name: "sshKeys",
create: postgresCmd{SSHKeys: []storage.SSHKey{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJGG5/nnivrW4zLD4ANLclVT3y68GAg6NOA3HpzFLo5e test@test"}},
want: storage.PostgresParameters{SSHKeys: []storage.SSHKey{"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJGG5/nnivrW4zLD4ANLclVT3y68GAg6NOA3HpzFLo5e test@test"}},
},
{
name: "allowedCIDRs",
create: postgresCmd{AllowedCidrs: []meta.IPv4CIDR{meta.IPv4CIDR("0.0.0.0/0")}},
want: storage.PostgresParameters{AllowedCIDRs: []meta.IPv4CIDR{meta.IPv4CIDR("0.0.0.0/0")}},
},
{
name: "version",
create: postgresCmd{PostgresVersion: storage.PostgresVersionDefault},
want: storage.PostgresParameters{Version: storage.PostgresVersionDefault},
},
{
name: "keepDailyBackups",
create: postgresCmd{KeepDailyBackups: ptr.To(5)},
want: storage.PostgresParameters{KeepDailyBackups: ptr.To(5)},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.create.Name = "test-" + t.Name()
tt.create.Wait = false
tt.create.WaitTimeout = time.Second

scheme, err := api.NewScheme()
if err != nil {
t.Fatal(err)
}
builder := fake.NewClientBuilder().WithScheme(scheme)
if tt.interceptorFuncs != nil {
builder = builder.WithInterceptorFuncs(*tt.interceptorFuncs)
}
postgresClient := builder.Build()
apiClient := &api.Client{WithWatch: postgresClient, Project: "default"}
ctx := context.Background()

if err := tt.create.Run(ctx, apiClient); (err != nil) != tt.wantErr {
t.Errorf("postgresCmd.Run() error = %v, wantErr %v", err, tt.wantErr)
}

created := &storage.Postgres{ObjectMeta: metav1.ObjectMeta{Name: tt.create.Name, Namespace: apiClient.Project}}
if err := apiClient.Get(ctx, api.ObjectName(created), created); (err != nil) != tt.wantErr {
t.Fatalf("expected postgres to exist, got: %s", err)
}
if tt.wantErr {
return
}

// we set defaults for the slices
if tt.want.AllowedCIDRs == nil {
tt.want.AllowedCIDRs = []meta.IPv4CIDR{}
}
if tt.want.SSHKeys == nil {
tt.want.SSHKeys = []storage.SSHKey{}
}

if !reflect.DeepEqual(created.Spec.ForProvider, tt.want) {
t.Fatalf("expected postgres.Spec.ForProvider = %+v, got: %+v", created.Spec.ForProvider, tt.want)
}
})
}
}
1 change: 1 addition & 0 deletions delete/delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type Cmd struct {
Config configCmd `cmd:"" group:"deplo.io" name:"config" help:"Delete a deplo.io Project Configuration. (Beta - requires access)"`
Application applicationCmd `cmd:"" group:"deplo.io" name:"application" aliases:"app" help:"Delete a deplo.io Application. (Beta - requires access)"`
MySQL mySQLCmd `cmd:"" group:"storage.nine.ch" name:"mysql" help:"Delete a MySQL instance."`
Postgres postgresCmd `cmd:"" group:"storage.nine.ch" name:"postgres" help:"Delete a PostgreSQL instance."`
KeyValueStore keyValueStoreCmd `cmd:"" group:"storage.nine.ch" name:"keyvaluestore" aliases:"kvs" help:"Delete a KeyValueStore instance."`
CloudVirtualMachine cloudVMCmd `cmd:"" group:"infrastructure.nine.ch" name:"cloudvirtualmachine" aliases:"cloudvm" help:"Delete a CloudVM."`
}
Expand Down
32 changes: 32 additions & 0 deletions delete/postgres.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package delete

import (
"context"
"fmt"
"time"

"k8s.io/apimachinery/pkg/types"

storage "github.com/ninech/apis/storage/v1alpha1"
"github.com/ninech/nctl/api"
)

type postgresCmd struct {
Name string `arg:"" help:"Name of the PostgreSQL resource."`
Force bool `default:"false" help:"Do not ask for confirmation of deletion."`
Wait bool `default:"true" help:"Wait until PostgreSQL is fully deleted."`
WaitTimeout time.Duration `default:"300s" help:"Duration to wait for the deletion. Only relevant if wait is set."`
}

func (cmd *postgresCmd) Run(ctx context.Context, client *api.Client) error {
ctx, cancel := context.WithTimeout(ctx, cmd.WaitTimeout)
defer cancel()

postgres := &storage.Postgres{}
postgresName := types.NamespacedName{Name: cmd.Name, Namespace: client.Project}
if err := client.Get(ctx, postgresName, postgres); err != nil {
return fmt.Errorf("unable to get postgres %q: %w", postgres.Name, err)
}

return newDeleter(postgres, storage.PostgresKind).deleteResource(ctx, client, cmd.WaitTimeout, cmd.Wait, cmd.Force)
}
Loading

0 comments on commit 8ec00aa

Please sign in to comment.