Skip to content

Commit e5a8f98

Browse files
committed
feat(postgresdatabase): add commands
1 parent bcdd542 commit e5a8f98

16 files changed

+846
-10
lines changed

create/create.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ type Cmd struct {
2929
Application applicationCmd `cmd:"" group:"deplo.io" name:"application" aliases:"app,application" help:"Create a new deplo.io Application."`
3030
MySQL mySQLCmd `cmd:"" group:"storage.nine.ch" name:"mysql" help:"Create a new MySQL instance."`
3131
Postgres postgresCmd `cmd:"" group:"storage.nine.ch" name:"postgres" help:"Create a new PostgreSQL instance."`
32+
PostgresDatabase postgresDatabaseCmd `cmd:"" group:"storage.nine.ch" name:"postgresdatabase" help:"Create a new PostgreSQL database."`
3233
KeyValueStore keyValueStoreCmd `cmd:"" group:"storage.nine.ch" name:"keyvaluestore" aliases:"kvs" help:"Create a new KeyValueStore instance"`
3334
CloudVirtualMachine cloudVMCmd `cmd:"" group:"infrastructure.nine.ch" name:"cloudvirtualmachine" aliases:"cloudvm" help:"Create a new CloudVM."`
3435
}

create/postgresdatabase.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package create
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
"k8s.io/apimachinery/pkg/watch"
10+
11+
"github.com/alecthomas/kong"
12+
runtimev1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
13+
meta "github.com/ninech/apis/meta/v1alpha1"
14+
storage "github.com/ninech/apis/storage/v1alpha1"
15+
16+
"github.com/ninech/nctl/api"
17+
)
18+
19+
type postgresDatabaseCmd struct {
20+
resourceCmd
21+
Location string `placeholder:"${postgresdatabase_location_default}" help:"Location where the PostgreSQL database is created. Available locations are: ${postgresdatabase_location_options}"`
22+
PostgresDatabaseVersion storage.PostgresVersion `placeholder:"${postgresdatabase_version_default}" help:"Release version with which the PostgreSQL database is created. Available versions: ${postgresdatabase_versions}"`
23+
}
24+
25+
func (cmd *postgresDatabaseCmd) Run(ctx context.Context, client *api.Client) error {
26+
fmt.Printf("Creating new PostgresDatabase. (waiting up to %s).\n", cmd.WaitTimeout)
27+
postgresDatabase := cmd.newPostgresDatabase(client.Project)
28+
29+
c := newCreator(client, postgresDatabase, "postgresdatabase")
30+
ctx, cancel := context.WithTimeout(ctx, cmd.WaitTimeout)
31+
defer cancel()
32+
33+
if err := c.createResource(ctx); err != nil {
34+
return err
35+
}
36+
37+
if !cmd.Wait {
38+
return nil
39+
}
40+
41+
return c.wait(ctx, waitStage{
42+
objectList: &storage.PostgresDatabaseList{},
43+
onResult: func(event watch.Event) (bool, error) {
44+
if pdb, ok := event.Object.(*storage.PostgresDatabase); ok {
45+
return isAvailable(pdb), nil
46+
}
47+
return false, nil
48+
},
49+
},
50+
)
51+
}
52+
53+
func (cmd *postgresDatabaseCmd) newPostgresDatabase(namespace string) *storage.PostgresDatabase {
54+
name := getName(cmd.Name)
55+
56+
postgresDatabase := &storage.PostgresDatabase{
57+
ObjectMeta: metav1.ObjectMeta{
58+
Name: name,
59+
Namespace: namespace,
60+
},
61+
Spec: storage.PostgresDatabaseSpec{
62+
ResourceSpec: runtimev1.ResourceSpec{
63+
WriteConnectionSecretToReference: &runtimev1.SecretReference{
64+
Name: "postgresdatabase-" + name,
65+
Namespace: namespace,
66+
},
67+
},
68+
ForProvider: storage.PostgresDatabaseParameters{
69+
Location: meta.LocationName(cmd.Location),
70+
Version: cmd.PostgresDatabaseVersion,
71+
},
72+
},
73+
}
74+
75+
return postgresDatabase
76+
}
77+
78+
// PostgresDatabaseKongVars returns all variables which are used in the PostgresDatabase
79+
// create command
80+
func PostgresDatabaseKongVars() kong.Vars {
81+
result := make(kong.Vars)
82+
result["postgresdatabase_location_default"] = string(storage.PostgresDatabaseLocationDefault)
83+
result["postgresdatabase_location_options"] = strings.Join(stringSlice(storage.PostgresDatabaseLocationOptions), ", ")
84+
result["postgresdatabase_version_default"] = string(storage.PostgresDatabaseVersionDefault)
85+
result["postgresdatabase_versions"] = strings.Join(stringSlice(storage.PostgresDatabaseVersions), ", ")
86+
87+
return result
88+
}

create/postgresdatabase_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package create
2+
3+
import (
4+
"context"
5+
"errors"
6+
"testing"
7+
"time"
8+
9+
"github.com/google/go-cmp/cmp"
10+
storage "github.com/ninech/apis/storage/v1alpha1"
11+
"github.com/ninech/nctl/api"
12+
"github.com/ninech/nctl/internal/test"
13+
"github.com/stretchr/testify/require"
14+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
15+
"sigs.k8s.io/controller-runtime/pkg/client"
16+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
17+
)
18+
19+
func TestPostgresDatabase(t *testing.T) {
20+
ctx := context.Background()
21+
22+
tests := []struct {
23+
name string
24+
create postgresDatabaseCmd
25+
want storage.PostgresDatabaseParameters
26+
wantErr bool
27+
interceptorFuncs *interceptor.Funcs
28+
}{
29+
{
30+
name: "simple",
31+
create: postgresDatabaseCmd{},
32+
want: storage.PostgresDatabaseParameters{},
33+
},
34+
{
35+
name: "simpleErrorOnCreation",
36+
create: postgresDatabaseCmd{},
37+
wantErr: true,
38+
interceptorFuncs: &interceptor.Funcs{
39+
Create: func(_ context.Context, _ client.WithWatch, _ client.Object, _ ...client.CreateOption) error {
40+
return errors.New("error on creation")
41+
},
42+
},
43+
},
44+
{
45+
name: "version",
46+
create: postgresDatabaseCmd{PostgresDatabaseVersion: storage.PostgresDatabaseVersionDefault},
47+
want: storage.PostgresDatabaseParameters{Version: storage.PostgresDatabaseVersionDefault},
48+
},
49+
}
50+
for _, tt := range tests {
51+
t.Run(tt.name, func(t *testing.T) {
52+
tt.create.Name = "test-" + t.Name()
53+
tt.create.Wait = false
54+
tt.create.WaitTimeout = time.Second
55+
56+
opts := []test.ClientSetupOption{}
57+
if tt.interceptorFuncs != nil {
58+
opts = append(opts, test.WithInterceptorFuncs(*tt.interceptorFuncs))
59+
}
60+
apiClient, err := test.SetupClient(opts...)
61+
require.NoError(t, err)
62+
63+
if err := tt.create.Run(ctx, apiClient); (err != nil) != tt.wantErr {
64+
t.Errorf("postgresDatabaseCmd.Run() error = %v, wantErr %v", err, tt.wantErr)
65+
}
66+
67+
created := &storage.PostgresDatabase{ObjectMeta: metav1.ObjectMeta{Name: tt.create.Name, Namespace: apiClient.Project}}
68+
if err := apiClient.Get(ctx, api.ObjectName(created), created); (err != nil) != tt.wantErr {
69+
t.Fatalf("expected postgresdatabase to exist, got: %s", err)
70+
}
71+
if tt.wantErr {
72+
return
73+
}
74+
75+
require.True(t, cmp.Equal(tt.want, created.Spec.ForProvider))
76+
})
77+
}
78+
}

delete/delete.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ type Cmd struct {
2121
Application applicationCmd `cmd:"" group:"deplo.io" name:"application" aliases:"app,application" help:"Delete a deplo.io Application."`
2222
MySQL mySQLCmd `cmd:"" group:"storage.nine.ch" name:"mysql" help:"Delete a MySQL instance."`
2323
Postgres postgresCmd `cmd:"" group:"storage.nine.ch" name:"postgres" help:"Delete a PostgreSQL instance."`
24+
PostgresDatabase postgresDatabaseCmd `cmd:"" group:"storage.nine.ch" name:"postgresdatabase" help:"Delete a PostgreSQL database."`
2425
KeyValueStore keyValueStoreCmd `cmd:"" group:"storage.nine.ch" name:"keyvaluestore" aliases:"kvs" help:"Delete a KeyValueStore instance."`
2526
CloudVirtualMachine cloudVMCmd `cmd:"" group:"infrastructure.nine.ch" name:"cloudvirtualmachine" aliases:"cloudvm" help:"Delete a CloudVM."`
2627
}

delete/postgresdatabase.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package delete
2+
3+
import (
4+
"context"
5+
6+
storage "github.com/ninech/apis/storage/v1alpha1"
7+
"github.com/ninech/nctl/api"
8+
)
9+
10+
type postgresDatabaseCmd struct {
11+
resourceCmd
12+
}
13+
14+
func (cmd *postgresDatabaseCmd) Run(ctx context.Context, client *api.Client) error {
15+
postgresDatabase := &storage.PostgresDatabase{}
16+
postgresDatabase.SetName(cmd.Name)
17+
postgresDatabase.SetNamespace(client.Project)
18+
19+
return newDeleter(postgresDatabase, storage.PostgresDatabaseKind).
20+
deleteResource(ctx, client, cmd.WaitTimeout, cmd.Wait, cmd.Force)
21+
}

delete/postgresdatabase_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package delete
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"github.com/ninech/nctl/api"
9+
"github.com/ninech/nctl/internal/test"
10+
"github.com/stretchr/testify/require"
11+
"k8s.io/apimachinery/pkg/api/errors"
12+
)
13+
14+
func TestPostgresDatabase(t *testing.T) {
15+
ctx := context.Background()
16+
cmd := postgresDatabaseCmd{
17+
resourceCmd: resourceCmd{
18+
Name: "test",
19+
Force: true,
20+
Wait: false,
21+
WaitTimeout: time.Second,
22+
},
23+
}
24+
25+
postgresDatabase := test.PostgresDatabase("test", test.DefaultProject, "nine-es34")
26+
27+
apiClient, err := test.SetupClient()
28+
require.NoError(t, err)
29+
30+
if err := apiClient.Create(ctx, postgresDatabase); err != nil {
31+
t.Fatalf("postgresdatabase create error, got: %s", err)
32+
}
33+
if err := apiClient.Get(ctx, api.ObjectName(postgresDatabase), postgresDatabase); err != nil {
34+
t.Fatalf("expected postgresdatabase to exist, got: %s", err)
35+
}
36+
if err := cmd.Run(ctx, apiClient); err != nil {
37+
t.Fatal(err)
38+
}
39+
err = apiClient.Get(ctx, api.ObjectName(postgresDatabase), postgresDatabase)
40+
if err == nil {
41+
t.Fatalf("expected postgresdatabase to be deleted, but exists")
42+
}
43+
if !errors.IsNotFound(err) {
44+
t.Fatalf("expected postgresdatabase to be deleted, got: %s", err.Error())
45+
}
46+
}

get/database.go

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package get
2+
3+
import (
4+
"context"
5+
"io"
6+
"text/tabwriter"
7+
8+
"github.com/crossplane/crossplane-runtime/pkg/resource"
9+
"github.com/ninech/nctl/api"
10+
"github.com/ninech/nctl/internal/format"
11+
runtimeclient "sigs.k8s.io/controller-runtime/pkg/client"
12+
)
13+
14+
type Database struct {
15+
Namespace, Name, FQDN, Location, Size, Connections string
16+
}
17+
18+
type databaseCmd struct {
19+
resourceCmd
20+
PrintPassword bool `help:"Print the password of the database. Requires name to be set." xor:"print"`
21+
PrintDatabaseUser bool `help:"Print the database name and user of the database. Requires name to be set." xor:"print"`
22+
PrintConnectionString bool `help:"Print the connection string of the database. Requires name to be set." xor:"print"`
23+
24+
out io.Writer
25+
}
26+
27+
func (cmd *databaseCmd) runDatabaseCmd(ctx context.Context, client *api.Client, get *Cmd,
28+
databaseList runtimeclient.ObjectList, databaseResources []resource.Managed, databaseKind string,
29+
connectionStringHandler func(context.Context, *api.Client, resource.Managed) error,
30+
databasesHandler func([]resource.Managed, *Cmd, bool) error,
31+
) error {
32+
cmd.out = defaultOut(cmd.out)
33+
34+
if err := get.list(ctx, client, databaseList, api.MatchName(cmd.Name)); err != nil {
35+
return err
36+
}
37+
38+
if len(databaseResources) == 0 {
39+
get.printEmptyMessage(cmd.out, databaseKind, client.Project)
40+
return nil
41+
}
42+
43+
if cmd.Name != "" && cmd.PrintConnectionString {
44+
return connectionStringHandler(ctx, client, databaseResources[0])
45+
}
46+
47+
if cmd.Name != "" && cmd.PrintDatabaseUser {
48+
return cmd.printSecret(cmd.out, ctx, client, databaseResources[0], func(db, _ string) string { return db })
49+
}
50+
51+
if cmd.Name != "" && cmd.PrintPassword {
52+
return cmd.printSecret(cmd.out, ctx, client, databaseResources[0], func(_, pw string) string { return pw })
53+
}
54+
55+
switch get.Output {
56+
case full:
57+
return databasesHandler(databaseResources, get, true)
58+
case noHeader:
59+
return databasesHandler(databaseResources, get, false)
60+
case yamlOut:
61+
return format.PrettyPrintObjects(databaseResources, format.PrintOpts{})
62+
case jsonOut:
63+
return format.PrettyPrintObjects(
64+
databaseResources,
65+
format.PrintOpts{
66+
Format: format.OutputFormatTypeJSON,
67+
JSONOpts: format.JSONOutputOptions{
68+
PrintSingleItem: cmd.Name != "",
69+
},
70+
})
71+
}
72+
73+
return nil
74+
}
75+
76+
func printDatabases(out io.Writer, get *Cmd, databases []Database, header bool) error {
77+
w := tabwriter.NewWriter(out, 0, 0, 5, ' ', 0)
78+
79+
if header {
80+
get.writeHeader(w, "NAME", "FQDN", "LOCATION", "SIZE", "CONNECTIONS")
81+
}
82+
83+
for _, db := range databases {
84+
get.writeTabRow(w,
85+
db.Namespace,
86+
db.Name,
87+
db.FQDN,
88+
db.Location,
89+
db.Size,
90+
db.Connections,
91+
)
92+
}
93+
94+
return w.Flush()
95+
}

0 commit comments

Comments
 (0)