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

feat(postgres): Add resource #103

Merged
merged 2 commits into from
Jun 6, 2024
Merged
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
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