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(keyvaluestore): add KeyValueStore support #83

Merged
merged 23 commits into from
Apr 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
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 .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