Skip to content

Commit

Permalink
Supply NuoDB license declaratively via secret (#336)
Browse files Browse the repository at this point in the history
Mount NuoDB license declaratively using Secret resource.
This requires NuoDB image v5.1.1 and above.
  • Loading branch information
sivanov-nuodb authored Nov 6, 2023
1 parent 56718a7 commit c7b3e1c
Show file tree
Hide file tree
Showing 7 changed files with 171 additions and 19 deletions.
4 changes: 3 additions & 1 deletion stable/admin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,8 @@ The following tables list the configurable parameters for the `admin` option of
| `domain` | NuoDB admin cluster name | `nuodb` |
| `namespace` | Namespace where admin is deployed; when peering to an existing admin cluster provide its project name | `nuodb` |
| `replicas` | Number of NuoDB Admin replicas | `1` |
| `license.secret` | The name of the Secret resource for NuoDB license. Supported starting from NuoDB image version 5.1.1 | `""` |
| `license.key` | The key which value has the NuoDB license contents | `nuodb.lic` |
| `lbConfig.prefilter` | Global load balancer prefilter expression | `nil` |
| `lbConfig.default` | Global load balancer default query | `nil` |
| `lbConfig.policies` | Load balancer named policies | `{ nearest: ... }` |
Expand All @@ -166,7 +168,7 @@ The following tables list the configurable parameters for the `admin` option of
| `affinity` | Affinity rules for NuoDB Admin | `{}` |
| `nodeSelector` | Node selector rules for NuoDB Admin | `{}` |
| `tolerations` | Tolerations for NuoDB Admin | `[]` |
| `configFilesPath` | Directory path where `configFiles.*` are found | `/etc/nuodb/` |
| `configFilesPath` | Directory path where `configFiles.*` are found | `/etc/nuodb` |
| `configFiles.*` | See below. | `{}` |
| `persistence.accessModes` | Volume access modes enabled (must match capabilities of the storage class) | `ReadWriteMany` |
| `persistence.size` | Amount of disk space allocated for admin RAFT state | `10Gi` |
Expand Down
19 changes: 18 additions & 1 deletion stable/admin/templates/statefulset.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ spec:
- "tdeMonitor.storagePasswordsDir={{ .Values.admin.tde.storagePasswordsDir }}"
{{- end }}
{{- end }}
{{- if .Values.admin.license.secret }}
- "licenseFilePath={{ .Values.admin.configFilesPath }}/license/nuodb.lic"
{{- end }}
{{- range $opt, $val := .Values.admin.options}}
- "{{$opt}}={{$val}}"
{{- end }}
Expand Down Expand Up @@ -198,10 +201,15 @@ spec:
{{- with .Values.admin.configFiles }}
{{- range $key, $val := . }}
- name: configurations
mountPath: {{ $.Values.admin.configFilesPath }}{{ $key }}
mountPath: {{ $.Values.admin.configFilesPath }}/{{ $key }}
subPath: {{ $key }}
{{- end -}}
{{- end }}
{{- if .Values.admin.license.secret }}
- name: license
mountPath: {{ .Values.admin.configFilesPath }}/license
readOnly: true
{{- end }}
- name: raftlog
mountPath: /var/opt/nuodb
- name: nuoadmin
Expand Down Expand Up @@ -251,6 +259,15 @@ spec:
configMap:
name: {{ template "admin.fullname" . }}-configuration
{{- end }}
{{- if .Values.admin.license.secret }}
- name: license
secret:
secretName: {{ .Values.admin.license.secret }}
defaultMode: 0440
items:
- key: {{ .Values.admin.license.key }}
path: nuodb.lic
{{- end }}
- name: nuoadmin
configMap:
name: {{ template "admin.fullname" . }}-nuoadmin
Expand Down
17 changes: 13 additions & 4 deletions stable/admin/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -302,12 +302,21 @@ admin:
# memory: 16Mi

# Custom NuoDB configuration files path
configFilesPath: /etc/nuodb/
configFilesPath: /etc/nuodb

# NuoDB is a licensed product for Enterprise Edition.
# Obtain your license from NuoDB support.
# NuoDB is a licensed product for Enterprise Edition. Obtain your license from
# NuoDB support at support@nuodb.com.
#
# You can provide the license via a configFile by using:
# You can provide the license via Kubernetes secret which is supported
# starting with NuoDB image version 5.1.1.
#
license:
# The name of the Secret resource.
secret: ""
# The key which value has the NuoDB license contents.
key: "nuodb.lic"

# Alternatively, you can provide the license via a configFile by using:
#
# - Helm CLI parameter
# - Specify in this values.yaml file
Expand Down
39 changes: 37 additions & 2 deletions test/integration/template_admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ func verifyTLSSecrets(t assert.TestingT, spec v1.PodSpec, options *helm.Options)
assert.True(t, found, "Expected to find tls volume")
assert.NotNil(t, tlsVolume.Projected)
assert.NotNil(t, tlsVolume.Projected.DefaultMode)
// volume mode 0440 is int32(288)
assert.Equal(t, int32(288), *tlsVolume.Projected.DefaultMode)
assert.Equal(t, int32(0440), *tlsVolume.Projected.DefaultMode)
for _, tlsKeyPrefix := range []string{"admin.tlsClientPEM", "admin.tlsCACert", "admin.tlsKeyStore", "admin.tlsTrustStore"} {
secretNameValue := fmt.Sprintf("%s.secret", tlsKeyPrefix)
if secretName, ok := options.SetValues[secretNameValue]; ok {
Expand Down Expand Up @@ -1504,3 +1503,39 @@ func TestAdminTLSConfig(t *testing.T) {
}
})
}

func TestAdminLicenseFromSecret(t *testing.T) {
// Path to the helm chart we will test
helmChartPath := testlib.ADMIN_HELM_CHART_PATH

options := &helm.Options{
SetValues: map[string]string{
"admin.license.secret": "nuodb-license",
"admin.license.key": "content",
},
}

// Render and decode StatefulSets
output := helm.RenderTemplate(t, options, helmChartPath, "release-name", []string{"templates/statefulset.yaml"})
for _, obj := range testlib.SplitAndRenderStatefulSet(t, output, 1) {
// Verify the license volume
licenseVolume, found := testlib.GetVolume(obj.Spec.Template.Spec.Volumes, "license")
assert.True(t, found, "Expected to find license volume")
assert.NotNil(t, licenseVolume.Secret)
assert.NotNil(t, licenseVolume.Secret.DefaultMode)
assert.Equal(t, int32(0440), *licenseVolume.Secret.DefaultMode)
assert.Equal(t, options.SetValues["admin.license.secret"], licenseVolume.Secret.SecretName)
assert.Len(t, licenseVolume.Secret.Items, 1, "Expected to find nuodb.lic item")
assert.Equal(t, options.SetValues["admin.license.key"], licenseVolume.Secret.Items[0].Key)
assert.Equal(t, "nuodb.lic", licenseVolume.Secret.Items[0].Path)

// Verify the license volume mount
licenseVolumeMount, found := testlib.GetMount(obj.Spec.Template.Spec.Containers[0].VolumeMounts, "license")
assert.True(t, found, "Expected to find license volume mount")
assert.Equal(t, "/etc/nuodb/license", licenseVolumeMount.MountPath)
assert.True(t, licenseVolumeMount.ReadOnly)

// Verify the NuoAdmin config option
assert.Contains(t, obj.Spec.Template.Spec.Containers[0].Args, "licenseFilePath=/etc/nuodb/license/nuodb.lic")
}
}
72 changes: 71 additions & 1 deletion test/minikube/minikube_base_admin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,29 @@
package minikube

import (
"context"
"encoding/base64"
"fmt"
"os"
"strings"
"testing"
"time"

"github.com/gruntwork-io/terratest/modules/helm"
"github.com/gruntwork-io/terratest/modules/k8s"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/stretchr/testify/require"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"

"github.com/nuodb/nuodb-helm-charts/v3/test/testlib"
)

func TestKubernetesBasicAdminSingleReplica(t *testing.T) {
testlib.AwaitTillerUp(t)
defer testlib.VerifyTeardown(t)

options := helm.Options {
options := helm.Options{
SetValues: map[string]string{},
}

Expand Down Expand Up @@ -50,6 +60,66 @@ func TestKubernetesBasicAdminSingleReplica(t *testing.T) {
})
}

func TestKubernetesAdminLicenseSecret(t *testing.T) {
testlib.SkipTestOnNuoDBVersionCondition(t, "< 5.1.1")
if os.Getenv("NUODB_LICENSE_CONTENT") == "" {
t.Skip("Cannot run this test without a valid license")
}
testlib.AwaitTillerUp(t)
defer testlib.VerifyTeardown(t)

randomSuffix := strings.ToLower(random.UniqueId())
namespaceName := fmt.Sprintf("%skubernetesadminlicensesecret-%s", testlib.NAMESPACE_NAME_PREFIX, randomSuffix)
testlib.CreateNamespace(t, namespaceName)

ctx := context.Background()
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: "nuodb-license",
Namespace: namespaceName,
},
StringData: map[string]string{
"nuodb.lic": "garbage",
},
}

options := helm.Options{
SetValues: map[string]string{
"admin.license.secret": "nuodb-license",
},
}
kubectlOptions := k8s.NewKubectlOptions("", "", namespaceName)

clientset, err := k8s.GetKubernetesClientFromOptionsE(t, kubectlOptions)
require.NoError(t, err)
clientset.CoreV1().Secrets(namespaceName).Create(ctx, secret, metav1.CreateOptions{})

defer testlib.Teardown(testlib.TEARDOWN_ADMIN)

helmChartReleaseName, _ := testlib.StartAdmin(t, &options, 1, namespaceName)

admin0 := fmt.Sprintf("%s-nuodb-cluster0-0", helmChartReleaseName)
testlib.VerifyLicense(t, namespaceName, admin0, testlib.UNLICENSED)
testlib.VerifyLicensingErrorsInLog(t, namespaceName, admin0, true)

// Update the secret with the NuoDB EE license
licenseContentBytes, err := base64.StdEncoding.DecodeString(os.Getenv("NUODB_LICENSE_CONTENT"))
require.NoError(t, err)
secret, err = clientset.CoreV1().Secrets(namespaceName).Get(ctx, secret.Name, metav1.GetOptions{})
require.NoError(t, err)
secret.Data["nuodb.lic"] = licenseContentBytes
secret, err = clientset.CoreV1().Secrets(namespaceName).Update(ctx, secret, metav1.UpdateOptions{})
require.NoError(t, err)

t.Log("Waiting for license update")
testlib.Await(t, func() bool {
if err := testlib.VerifyLicenseE(t, namespaceName, admin0, testlib.ENTERPRISE); err != nil {
return false
}
return true
}, 180*time.Second)
}

func TestKubernetesInvalidLicense(t *testing.T) {
testlib.SkipTestOnNuoDBVersionCondition(t, ">= 6.0.0")
testlib.AwaitTillerUp(t)
Expand Down
30 changes: 22 additions & 8 deletions test/testlib/minikube_utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -832,22 +832,36 @@ func VerifyLicenseFile(t *testing.T, namespace string, podName string, expectedL
}

func VerifyLicense(t *testing.T, namespace string, podName string, expected LicenseType) {
options := k8s.NewKubectlOptions("", "", namespace)
require.NoError(t, VerifyLicenseE(t, namespace, podName, expected))
}

output, err := k8s.RunKubectlAndGetOutputE(t, options, "exec", "-c", "admin", podName, "--", "nuocmd", "show", "domain")
require.NoError(t, err)
func VerifyLicenseE(t *testing.T, namespace string, podName string, expected LicenseType) error {
options := k8s.NewKubectlOptions("", "", namespace)

expectedString := fmt.Sprintf("server license: %s", expected)
output, err := k8s.RunKubectlAndGetOutputE(t, options, "exec", podName, "-c", "admin", "--",
"nuocmd", "--show-json-fields", "decodedLicense.type", "get", "effective-license")
if err != nil {
return err
}
license := struct {
Type string `json:"decodedLicense.type"`
}{}
err = json.Unmarshal([]byte(output), &license)
if err != nil {
return fmt.Errorf("failed to decode license type: %w", err)
}
expectedString := fmt.Sprintf("%s", expected)
if expected != ENTERPRISE {
RunOnNuoDBVersionCondition(t, "<6.0.0", func(version *semver.Version) {
// Prior to NuoDB v6.0.0, Community and Enterprise license types are
// supported
expectedString = "server license: Community"
expectedString = "COMMUNITY"
})
}
output = strings.ToLower(output)
expectedString = strings.ToLower(expectedString)
require.True(t, strings.Contains(output, expectedString), output)
if license.Type != expectedString {
return fmt.Errorf("unexpected license type expected=%q, found=%q", expectedString, license.Type)
}
return nil
}

// Deprecated: The VerifyLicenseIsCommunity function has been deprecated. When testing with NuoDB v6.0.0, use the VerifyLicense function instead.
Expand Down
9 changes: 7 additions & 2 deletions test/testlib/nuodb_admin_utilities.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,7 @@ func StartAdminTemplate(t *testing.T, options *helm.Options, replicaCount int, n

// If the nuodb.lic is provided explicitly then don't apply the
// license from the env variable
if values, ok := options.SetValues["admin.configFiles.nuodb\\.lic"]; !ok || values == "" {

if !isLicenseSetInValues(options) {
// LIMITED license takes priority over Enterprise license because
// we don't want to change the test flow for NuoDB version 6.0.
// Any test which requires Enterprise license would install it using
Expand All @@ -168,6 +167,12 @@ func StartAdminTemplate(t *testing.T, options *helm.Options, replicaCount int, n
return
}

func isLicenseSetInValues(options *helm.Options) bool {
licFile := options.SetValues["admin.configFiles.nuodb\\.lic"]
licSecret := options.SetValues["admin.license.secret"]
return licFile != "" || licSecret != ""
}

func InstallAdmin(t *testing.T, options *helm.Options, helmChartReleaseName string) {
if options.Version == "" {
helm.Install(t, options, ADMIN_HELM_CHART_PATH, helmChartReleaseName)
Expand Down

0 comments on commit c7b3e1c

Please sign in to comment.