Skip to content

Commit

Permalink
feat(keyvaluestore): add KeyValueStore support (#83)
Browse files Browse the repository at this point in the history
- use updated API definitions
- add support for KeyValueStore

---------

Co-authored-by: Josua Bryner <[email protected]>
Co-authored-by: Daniel Wilhelm <[email protected]>
  • Loading branch information
3 people authored Apr 25, 2024
1 parent daf08a7 commit 4f35071
Show file tree
Hide file tree
Showing 17 changed files with 663 additions and 29 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ dist/
### vscode ###
.vscode/*
*.code-workspace
/.devcontainer

### MacOS ###
.DS_Store
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."`
KeyValueStore keyValueStoreCmd `cmd:"" group:"storage.nine.ch" name:"keyvaluestore" aliases:"kvs" help:"Create a new KeyValueStore instance"`
}

// resultFunc is the function called on a watch event during creation. It
Expand Down
90 changes: 90 additions & 0 deletions create/keyvaluestore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package create

import (
"context"
"fmt"
"time"

runtimev1 "github.com/crossplane/crossplane-runtime/apis/common/v1"
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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
)

type keyValueStoreCmd struct {
Name string `arg:"" default:"" help:"Name of the KeyValueStore instance. A random name is generated if omitted."`
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"`
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."`
}

func (cmd *keyValueStoreCmd) Run(ctx context.Context, client *api.Client) error {
keyValueStore, err := cmd.newKeyValueStore(client.Project)
if err != nil {
return err
}

c := newCreator(client, keyValueStore, "keyvaluestore")
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.KeyValueStoreList{},
onResult: func(event watch.Event) (bool, error) {
if c, ok := event.Object.(*storage.KeyValueStore); ok {
return isAvailable(c), nil
}
return false, nil
},
},
)
}

func (cmd *keyValueStoreCmd) newKeyValueStore(namespace string) (*storage.KeyValueStore, error) {
name := getName(cmd.Name)

keyValueStore := &storage.KeyValueStore{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: storage.KeyValueStoreSpec{
ResourceSpec: runtimev1.ResourceSpec{
WriteConnectionSecretToReference: &runtimev1.SecretReference{
Name: "keyvaluestore-" + name,
Namespace: namespace,
},
},
ForProvider: storage.KeyValueStoreParameters{
Location: meta.LocationName(cmd.Location),
MaxMemoryPolicy: cmd.MaxMemoryPolicy,
AllowedCIDRs: cmd.AllowedCidrs,
},
},
}

if cmd.MemorySize != "" {
q, err := resource.ParseQuantity(cmd.MemorySize)
if err != nil {
return keyValueStore, fmt.Errorf("error parsing memory size %q: %w", cmd.MemorySize, err)
}

keyValueStore.Spec.ForProvider.MemorySize = &storage.KeyValueStoreMemorySize{Quantity: q}
}

return keyValueStore, nil
}
80 changes: 80 additions & 0 deletions create/keyvaluestore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package create

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

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

func TestKeyValueStore(t *testing.T) {
tests := []struct {
name string
create keyValueStoreCmd
want storage.KeyValueStoreParameters
wantErr bool
}{
{"simple", keyValueStoreCmd{}, storage.KeyValueStoreParameters{}, false},
{
"memorySize",
keyValueStoreCmd{MemorySize: "1G"},
storage.KeyValueStoreParameters{MemorySize: &storage.KeyValueStoreMemorySize{Quantity: resource.MustParse("1G")}},
false,
},
{
"maxMemoryPolicy",
keyValueStoreCmd{MaxMemoryPolicy: storage.KeyValueStoreMaxMemoryPolicy("noeviction")},
storage.KeyValueStoreParameters{MaxMemoryPolicy: storage.KeyValueStoreMaxMemoryPolicy("noeviction")},
false,
},
{
"allowedCIDRs",
keyValueStoreCmd{AllowedCidrs: []storage.IPv4CIDR{storage.IPv4CIDR("0.0.0.0/0")}},
storage.KeyValueStoreParameters{AllowedCIDRs: []storage.IPv4CIDR{storage.IPv4CIDR("0.0.0.0/0")}},
false,
},
{
"invalid",
keyValueStoreCmd{MemorySize: "invalid"},
storage.KeyValueStoreParameters{},
true,
},
}
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)
}
client := fake.NewClientBuilder().WithScheme(scheme).Build()
apiClient := &api.Client{WithWatch: client, Project: "default"}
ctx := context.Background()

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

created := &storage.KeyValueStore{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 keyvaluestore to exist, got: %s", err)
}
if tt.wantErr {
return
}

if !reflect.DeepEqual(created.Spec.ForProvider, tt.want) {
t.Fatalf("expected KeyValueStore.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."`
KeyValueStore keyValueStoreCmd `cmd:"" group:"storage.nine.ch" name:"keyvaluestore" aliases:"kvs" help:"Delete a KeyValueStore instance."`
}

// cleanupFunc is called after the resource has been deleted in order to do
Expand Down
32 changes: 32 additions & 0 deletions delete/keyvaluestore.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 keyValueStoreCmd struct {
Name string `arg:"" help:"Name of the KeyValueStore resource."`
Force bool `default:"false" help:"Do not ask for confirmation of deletion."`
Wait bool `default:"true" help:"Wait until KeyValueStore is fully deleted."`
WaitTimeout time.Duration `default:"300s" help:"Duration to wait for the deletion. Only relevant if wait is set."`
}

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

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

return newDeleter(keyValueStore, storage.KeyValueStoreKind).deleteResource(ctx, client, cmd.WaitTimeout, cmd.Wait, cmd.Force)
}
48 changes: 48 additions & 0 deletions delete/keyvaluestore_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package delete

import (
"context"
"testing"
"time"

"github.com/ninech/nctl/api"
"github.com/ninech/nctl/internal/test"
"k8s.io/apimachinery/pkg/api/errors"
"sigs.k8s.io/controller-runtime/pkg/client/fake"
)

func TestKeyValueStore(t *testing.T) {
cmd := keyValueStoreCmd{
Name: "test",
Force: true,
Wait: false,
WaitTimeout: time.Second,
}

keyValueStore := test.KeyValueStore("test", "default", "nine-es34")

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

if err := apiClient.Create(ctx, keyValueStore); err != nil {
t.Fatalf("keyvaluestore create error, got: %s", err)
}
if err := apiClient.Get(ctx, api.ObjectName(keyValueStore), keyValueStore); err != nil {
t.Fatalf("expected keyvaluestore to exist, got: %s", err)
}
if err := cmd.Run(ctx, apiClient); err != nil {
t.Fatal(err)
}
err = apiClient.Get(ctx, api.ObjectName(keyValueStore), keyValueStore)
if err == nil {
t.Fatalf("expected keyvaluestore to be deleted, but exists")
}
if !errors.IsNotFound(err) {
t.Fatalf("expected keyvaluestore to be deleted, got: %s", err.Error())
}
}
10 changes: 2 additions & 8 deletions get/apiserviceaccount.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,18 +75,12 @@ func (asa *apiServiceAccountsCmd) print(sas []iam.APIServiceAccount, get *Cmd, h
}

func (asa *apiServiceAccountsCmd) printToken(ctx context.Context, client *api.Client, sa *iam.APIServiceAccount) error {
secret, err := client.GetConnectionSecret(ctx, sa)
token, err := getConnectionSecret(ctx, client, tokenKey, sa)
if err != nil {
return fmt.Errorf("unable to get connection secret: %w", err)
}

token, ok := secret.Data[tokenKey]
if !ok {
return fmt.Errorf("secret of API Service Account %s has no token", sa.Name)
return err
}

fmt.Printf("%s\n", token)

return nil
}

Expand Down
2 changes: 2 additions & 0 deletions get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type Cmd struct {
Releases releasesCmd `cmd:"" group:"deplo.io" name:"releases" aliases:"release" help:"Get deplo.io Releases. (Beta - requires access)"`
Configs configsCmd `cmd:"" group:"deplo.io" name:"configs" aliases:"config" help:"Get deplo.io Project Configuration. (Beta - requires access)"`
MySQL mySQLCmd `cmd:"" group:"storage.nine.ch" name:"mysql" help:"Get MySQL instances."`
KeyValueStore keyValueStoreCmd `cmd:"" group:"storage.nine.ch" name:"keyvaluestore" aliases:"kvs" help:"Get KeyValueStore instances."`
All allCmd `cmd:"" name:"all" help:"Get project content"`

opts []runtimeclient.ListOption
Expand All @@ -49,6 +50,7 @@ func matchName(name string) listOpt {
cmd.opts = append(cmd.opts, runtimeclient.MatchingFields{"metadata.name": name})
}
}

func matchLabel(k, v string) listOpt {
return func(cmd *Cmd) {
cmd.opts = append(cmd.opts, runtimeclient.MatchingLabels{k: v})
Expand Down
73 changes: 73 additions & 0 deletions get/keyvaluestore.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package get

import (
"context"
"fmt"
"io"
"text/tabwriter"

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

type keyValueStoreCmd struct {
Name string `arg:"" help:"Name of the KeyValueStore Instance to get. If omitted all in the project will be listed." default:""`
PrintToken bool `help:"Print the bearer token of the Account. Requires name to be set." default:"false"`

out io.Writer
}

func (cmd *keyValueStoreCmd) Run(ctx context.Context, client *api.Client, get *Cmd) error {
cmd.out = defaultOut(cmd.out)

keyValueStoreList := &storage.KeyValueStoreList{}

if err := get.list(ctx, client, keyValueStoreList, matchName(cmd.Name)); err != nil {
return err
}

if len(keyValueStoreList.Items) == 0 {
printEmptyMessage(cmd.out, storage.KeyValueStoreKind, client.Project)
return nil
}

if cmd.Name != "" && cmd.PrintToken {
return cmd.printPassword(ctx, client, &keyValueStoreList.Items[0])
}

switch get.Output {
case full:
return cmd.printKeyValueStoreInstances(keyValueStoreList.Items, get, true)
case noHeader:
return cmd.printKeyValueStoreInstances(keyValueStoreList.Items, get, false)
case yamlOut:
return format.PrettyPrintObjects(keyValueStoreList.GetItems(), format.PrintOpts{})
}

return nil
}

func (cmd *keyValueStoreCmd) printKeyValueStoreInstances(list []storage.KeyValueStore, get *Cmd, header bool) error {
w := tabwriter.NewWriter(cmd.out, 0, 0, 4, ' ', 0)

if header {
get.writeHeader(w, "NAME", "FQDN", "TLS", "MEMORY SIZE")
}

for _, keyValueStore := range list {
get.writeTabRow(w, keyValueStore.Namespace, keyValueStore.Name, keyValueStore.Status.AtProvider.FQDN, "true", keyValueStore.Spec.ForProvider.MemorySize.String())
}

return w.Flush()
}

func (cmd *keyValueStoreCmd) printPassword(ctx context.Context, client *api.Client, keyValueStore *storage.KeyValueStore) error {
pw, err := getConnectionSecret(ctx, client, storage.KeyValueStoreUser, keyValueStore)
if err != nil {
return err
}

fmt.Fprintln(cmd.out, pw)
return nil
}
Loading

0 comments on commit 4f35071

Please sign in to comment.