Skip to content
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
23 changes: 16 additions & 7 deletions internal/apply/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,22 +132,31 @@ spec:

```yaml
version: v1
kind: Valkey
metadata:
name: my-cache
type: Valkey
name: my-cache
spec:
tier: SingleNode # SingleNode | HighAvailability
memory: "4GB" # 1GB | 4GB | 8GB | 14GB | 28GB | 56GB | 112GB | 200GB
version: "9.1" # required; use a quoted version string
maxMemoryPolicy: allkeys-lru # optional; see below
databases: 16 # optional
notifyKeyspaceEvents: "Ex" # optional
persistence: # optional; parsed but not yet sent to the API
persistence: # optional
disabled: false
```

**maxMemoryPolicy values:** `allkeys-lfu`, `allkeys-lru`, `allkeys-random`,
`noeviction`, `volatile-lfu`, `volatile-lru`, `volatile-random`, `volatile-ttl`

Omitting `maxMemoryPolicy` uses the API default on creation and leaves the
existing policy unchanged on update.

Versions are converted to GraphQL enum names (e.g. `"9.1"` becomes `V9_1`),
without a CLI-maintained version mapping. The API must support the chosen version.
`persistence.disabled` controls RDB dumps and backups; setting it to `true`
means all data is lost on restart. Omitting it leaves the API default on creation
and the existing setting on update.

### OpenSearch

```yaml
Expand Down Expand Up @@ -194,12 +203,12 @@ binaryData: # optional; values must be base64-encoded

```yaml
version: v1
kind: Valkey
metadata:
name: sessions
type: Valkey
name: sessions
spec:
tier: HighAvailability
memory: "28GB"
version: "9.1"
---
version: v1
kind: OpenSearch
Expand Down
2 changes: 1 addition & 1 deletion internal/apply/resource/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ spec:
Databases: new(16),
MaxMemoryPolicy: "allkeys-lru",
NotifyKeyspaceEvents: "Ex",
Persistence: &valkeyPersistence{Disabled: false},
Persistence: &valkeyPersistence{Disabled: new(false)},
}
if diff := cmp.Diff(want, spec); diff != "" {
t.Errorf("spec mismatch (-want +got):\n%s", diff)
Expand Down
53 changes: 33 additions & 20 deletions internal/apply/resource/valkey.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package resource

import (
"context"
"strings"

"github.com/nais/cli/internal/naisapi"
"github.com/nais/cli/internal/naisapi/gql"
Expand All @@ -27,12 +28,11 @@ type valkeySpec struct {
Databases *int `yaml:"databases"`
NotifyKeyspaceEvents string `yaml:"notifyKeyspaceEvents"`
Persistence *valkeyPersistence `yaml:"persistence"`
Version string `yaml:"version"`
}

// valkeyPersistence is parsed so it does not trip the strict spec decoder, but
// is ignored until the nais-api mutation supports it.
type valkeyPersistence struct {
Disabled bool `yaml:"disabled"`
Disabled *bool `yaml:"disabled"`
}

var (
Expand Down Expand Up @@ -68,25 +68,10 @@ func (v valkeyResource) Apply(ctx context.Context, meta Metadata, m Manifest) (A
return "", err
}

data := &valkey.Valkey{
Databases: s.Databases,
NotifyKeyspaceEvents: s.NotifyKeyspaceEvents,
Labels: m.Labels,
}

var err error
if data.Memory, err = enumValue("memory", s.Memory, valkeyMemory); err != nil {
return "", err
}
if data.Tier, err = enumValue("tier", s.Tier, valkeyTier); err != nil {
data, err := s.toValkey(m.Labels)
if err != nil {
return "", err
}
if s.MaxMemoryPolicy != "" {
if data.MaxMemoryPolicy, err = enumValue("maxMemoryPolicy", s.MaxMemoryPolicy, valkeyMaxMemoryPolicy); err != nil {
return "", err
}
}
// s.Persistence is parsed but intentionally ignored until the API supports it.

vmeta := valkey.Metadata{
Name: m.Name,
Expand All @@ -110,6 +95,34 @@ func (v valkeyResource) Apply(ctx context.Context, meta Metadata, m Manifest) (A
return ActionCreated, nil
}

func (s valkeySpec) toValkey(labels map[string]string) (*valkey.Valkey, error) {
data := &valkey.Valkey{
Databases: s.Databases,
NotifyKeyspaceEvents: s.NotifyKeyspaceEvents,
Labels: labels,
}
if s.Version != "" {
data.Version = gql.ValkeyMajorVersion("V" + strings.ReplaceAll(s.Version, ".", "_"))
}
if s.Persistence != nil {
data.PersistenceDisabled = s.Persistence.Disabled
}

var err error
if data.Memory, err = enumValue("memory", s.Memory, valkeyMemory); err != nil {
return nil, err
}
if data.Tier, err = enumValue("tier", s.Tier, valkeyTier); err != nil {
return nil, err
}
if s.MaxMemoryPolicy != "" {
if data.MaxMemoryPolicy, err = enumValue("maxMemoryPolicy", s.MaxMemoryPolicy, valkeyMaxMemoryPolicy); err != nil {
return nil, err
}
}
return data, nil
}

// exists reports whether a Valkey instance with the given name already exists.
func (v valkeyResource) exists(ctx context.Context, meta valkey.Metadata) (bool, error) {
_, err := valkey.Get(ctx, meta)
Expand Down
72 changes: 72 additions & 0 deletions internal/apply/resource/valkey_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package resource

import (
"fmt"
"testing"

"github.com/google/go-cmp/cmp"
"github.com/nais/cli/internal/naisapi/gql"
"github.com/nais/cli/internal/valkey"
)

func TestValkeySpecToValkey(t *testing.T) {
for _, version := range []struct {
raw string
want gql.ValkeyMajorVersion
}{
{raw: "8.1", want: "V8_1"},
{raw: "9.0", want: "V9_0"},
{raw: "9.1", want: "V9_1"},
{raw: "10.2", want: "V10_2"},
} {
for _, persistence := range []struct {
name string
yaml string
want *bool
}{
{name: "omitted"},
{name: "empty", yaml: "\n persistence: {}"},
{name: "enabled", yaml: "\n persistence:\n disabled: false", want: new(false)},
{name: "disabled", yaml: "\n persistence:\n disabled: true", want: new(true)},
} {
t.Run(version.raw+"/"+persistence.name, func(t *testing.T) {
m, err := ParseManifest(mustDocument(t, fmt.Sprintf(`
version: v1
type: Valkey
name: my-cache
spec:
memory: 4GB
tier: SingleNode
version: %q
databases: 16
maxMemoryPolicy: allkeys-lru
notifyKeyspaceEvents: Ex
`, version.raw)+persistence.yaml))
if err != nil {
t.Fatal(err)
}
var spec valkeySpec
if err := decodeSpec(&m.Spec, &spec); err != nil {
t.Fatal(err)
}
data, err := spec.toValkey(map[string]string{"purpose": "cache"})
if err != nil {
t.Fatal(err)
}
want := &valkey.Valkey{
Memory: gql.ValkeyMemoryGb4,
Tier: gql.ValkeyTierSingleNode,
Version: version.want,
PersistenceDisabled: persistence.want,
Databases: new(16),
MaxMemoryPolicy: gql.ValkeyMaxMemoryPolicyAllkeysLru,
NotifyKeyspaceEvents: "Ex",
Labels: map[string]string{"purpose": "cache"},
}
if diff := cmp.Diff(want, data); diff != "" {
t.Errorf("Valkey mismatch (-want +got):\n%s", diff)
}
})
}
}
}
60 changes: 52 additions & 8 deletions internal/apply/validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,16 @@ const testResourceSchema = `{
"spec": {
"type": "object",
"additionalProperties": false,
"required": ["memory", "tier"],
"required": ["memory", "tier", "version"],
"properties": {
"memory": {"enum": ["1GB", "4GB"]},
"tier": {"enum": ["SingleNode", "HighAvailability"]}
"tier": {"enum": ["SingleNode", "HighAvailability"]},
"version": {"type": "string", "enum": ["8.1", "9.0", "9.1"]},
"persistence": {
"type": "object",
"additionalProperties": false,
"properties": {"disabled": {"type": "boolean"}}
}
}
}
}
Expand Down Expand Up @@ -122,6 +128,7 @@ name: my-valkey
spec:
memory: 1GB
tier: SingleNode
version: "9.1"
`)
if err != nil {
t.Fatalf("Run: %v\noutput:\n%s", err, out)
Expand All @@ -141,10 +148,39 @@ name: my-valkey
spec:
memory: 999GB
tier: SingleNode
version: "9.1"
`)
mustErrorContains(t, err, "Valkey/my-valkey")
}

func TestRun_DryRunValidatesValkeyVersion(t *testing.T) {
withTestResourceSchema(t, gojsonschema.NewStringLoader(testResourceSchema))

for _, version := range []string{`"8.1"`, `"9.0"`, `"9.1"`, `"10.2"`, `9.1`, ""} {
t.Run(version, func(t *testing.T) {
field := ""
if version != "" {
field = "\n version: " + version
}
out, err := writeManifestAndDryRun(t, `
version: v1
type: Valkey
name: my-valkey
spec:
memory: 1GB
tier: SingleNode`+field)
switch version {
case `"8.1"`, `"9.0"`, `"9.1"`:
if err != nil {
t.Fatalf("Run: %v\noutput:\n%s", err, out)
}
default:
mustErrorContains(t, err, "version")
}
})
}
}

func TestRun_DryRunRejectsUnknownNativeValkeySpecField(t *testing.T) {
withTestResourceSchema(t, gojsonschema.NewStringLoader(testResourceSchema))

Expand All @@ -155,6 +191,7 @@ name: my-valkey
spec:
memory: 1GB
tier: SingleNode
version: "9.1"
bogusField: nope
`)
mustErrorContains(t, err, "Valkey/my-valkey")
Expand All @@ -171,6 +208,7 @@ bogusTopLevel: nope
spec:
memory: 1GB
tier: SingleNode
version: "9.1"
`)
mustErrorContains(t, err, "Valkey/my-valkey")
mustErrorContains(t, err, "bogusTopLevel")
Expand Down Expand Up @@ -256,6 +294,7 @@ name: first
spec:
memory: 1GB
tier: SingleNode
version: "9.1"
---
version: v1
type: Postgres
Expand All @@ -282,6 +321,7 @@ name: first
spec:
memory: 1GB
tier: SingleNode
version: "9.1"
---
version: v1
type: Valkey
Expand All @@ -290,6 +330,7 @@ namespace: ignored
spec:
memory: 1GB
tier: SingleNode
version: "9.1"
`, dryRun, false)
mustErrorContains(t, err, "Valkey/second contains fields not used by nais apply: namespace")
mustErrorContains(t, err, "--allow-ignored-fields")
Expand All @@ -299,21 +340,23 @@ spec:
}
}

func TestRun_RejectsIgnoredValkeyPersistence(t *testing.T) {
func TestRun_DryRunAcceptsValkeyPersistence(t *testing.T) {
withTestResourceSchema(t, gojsonschema.NewStringLoader(testResourceSchema))

for _, dryRun := range []bool{false, true} {
_, err := writeManifestAndRun(t, `
for _, disabled := range []string{"true", "false"} {
out, err := writeManifestAndDryRun(t, `
version: v1
type: Valkey
name: my-valkey
spec:
memory: 1GB
tier: SingleNode
version: "9.1"
persistence:
disabled: true
`, dryRun, false)
mustErrorContains(t, err, "persistence")
disabled: `+disabled)
if err != nil {
t.Fatalf("Run: %v\noutput:\n%s", err, out)
}
}
}

Expand All @@ -328,6 +371,7 @@ namespace: ignored
spec:
memory: 1GB
tier: SingleNode
version: "9.1"
`, true, true)
if err != nil {
t.Fatalf("Run: %v\noutput:\n%s", err, out)
Expand Down
6 changes: 3 additions & 3 deletions internal/naisapi/gql/generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading