Skip to content

Commit

Permalink
Merge pull request #156 from vshn/mariadb-fix-settings
Browse files Browse the repository at this point in the history
Fix custom mariadb settings
  • Loading branch information
zugao authored Apr 29, 2024
2 parents 2973ba0 + c531d96 commit 8a1fea4
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 25 deletions.
23 changes: 2 additions & 21 deletions pkg/comp-functions/functions/common/backup/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ func AddBackupCMToValues(values map[string]any, volumePath []string, mountPath [
},
}

err := setNestedObjectValue(values, volumePath, volumes)
err := common.SetNestedObjectValue(values, volumePath, volumes)
if err != nil {
return err
}
Expand All @@ -241,29 +241,10 @@ func AddBackupCMToValues(values map[string]any, volumePath []string, mountPath [
MountPath: "/scripts",
},
}

err = setNestedObjectValue(values, mountPath, volumeMounts)
err = common.SetNestedObjectValue(values, mountPath, volumeMounts)
if err != nil {
return err
}

return nil
}

// setNestedObjectValue is necessary as unstructured can't handle anything except basic values and maps.
// this is a recursive function, it will traverse the map until it reaches the last element of the path.
// If it encounters any non-map values while traversing, it will throw an error.
func setNestedObjectValue(values map[string]interface{}, path []string, val interface{}) error {

if len(path) == 1 {
values[path[0]] = val
return nil
}

tmpVals, ok := values[path[0]].(map[string]interface{})
if !ok {
return fmt.Errorf("cannot traverse map, value at field %s is not a map", path[0])
}

return setNestedObjectValue(tmpVals, path[1:], val)
}
3 changes: 2 additions & 1 deletion pkg/comp-functions/functions/common/backup/backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package backup

import (
"context"
"github.com/vshn/appcat/v4/pkg/comp-functions/functions/common"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -103,7 +104,7 @@ func Test_setNestedValue(t *testing.T) {
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := setNestedObjectValue(tt.args.values, tt.args.path, tt.args.val); (err != nil) != tt.wantErr {
if err := common.SetNestedObjectValue(tt.args.values, tt.args.path, tt.args.val); (err != nil) != tt.wantErr {
t.Errorf("setNestedValue() error = %v, wantErr %v", err, tt.wantErr)
}
assert.Equal(t, tt.want, tt.args.values)
Expand Down
31 changes: 31 additions & 0 deletions pkg/comp-functions/functions/common/util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package common

import "fmt"

// SetNestedObjectValue is necessary as unstructured can't handle anything except basic values and maps.
// this is a recursive function, it will traverse the map until it reaches the last element of the path.
// If it encounters any non-map values while traversing, it will throw an error.
func SetNestedObjectValue(values map[string]interface{}, path []string, val interface{}) error {

if len(path) == 1 {
source, isSlice := values[path[0]].([]interface{})
if !isSlice {
values[path[0]] = val
return nil
}
toArrayValue, isSlice := val.([]interface{})
if !isSlice {
values[path[0]] = append(source, val)
return nil
}
values[path[0]] = append(source, toArrayValue...)
return nil
}

tmpVals, ok := values[path[0]].(map[string]interface{})
if !ok {
return fmt.Errorf("cannot traverse map, value at field %s is not a map", path[0])
}

return SetNestedObjectValue(tmpVals, path[1:], val)
}
183 changes: 181 additions & 2 deletions pkg/comp-functions/functions/vshnmariadb/mariadb_deploy.go
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
package vshnmariadb

import (
"bytes"
"compress/gzip"
"context"
"encoding/base64"
"encoding/json"
"fmt"

xfnproto "github.com/crossplane/function-sdk-go/proto/v1beta1"
xhelmbeta1 "github.com/vshn/appcat/v4/apis/helm/release/v1beta1"
vshnv1 "github.com/vshn/appcat/v4/apis/vshn/v1"
"github.com/vshn/appcat/v4/pkg/common/utils"
"github.com/vshn/appcat/v4/pkg/comp-functions/functions/common"
"github.com/vshn/appcat/v4/pkg/comp-functions/functions/common/maintenance"
"github.com/vshn/appcat/v4/pkg/comp-functions/runtime"
"io"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/utils/ptr"
"strconv"
)

const (
mariadbPort = "3306"
mariadbUser = "root"
extraConfig = "extra.cnf"
)

// DeployMariadb will deploy the objects to provision mariadb instance
Expand Down Expand Up @@ -70,6 +79,13 @@ func DeployMariadb(ctx context.Context, svc *runtime.ServiceRuntime) *xfnproto.R
if err != nil {
return runtime.NewWarningResult(fmt.Errorf("cannot get connection details: %w", err).Error())
}

l.Info("Observe default release secret in case of custom MariaDB settings")
if rSecretValues := getReleaseSecretValue(svc, comp); len(rSecretValues) > 0 && comp.Spec.Parameters.Service.MariadbSettings != "" {
if err = manageCustomDBSettings(svc, comp, rSecretValues); err != nil {
return runtime.NewWarningResult(fmt.Errorf("cannot set custom MariaDB settings: %w", err).Error())
}
}
return nil
}

Expand Down Expand Up @@ -172,7 +188,6 @@ func newValues(ctx context.Context, svc *runtime.ServiceRuntime, comp *vshnv1.VS
"certKeyFilename": "tls.key",
"certCAFilename": "ca.crt",
},
"mariadbConfiguration": comp.Spec.Parameters.Service.MariadbSettings,
"persistence": map[string]interface{}{
"size": res.Disk,
"storageClass": comp.Spec.Parameters.StorageClass,
Expand Down Expand Up @@ -214,9 +229,173 @@ func newRelease(ctx context.Context, svc *runtime.ServiceRuntime, values map[str
ToConnectionSecretKey: "ca.crt",
SkipPartOfReleaseCheck: true,
},
{
ObjectReference: corev1.ObjectReference{
APIVersion: "v1",
Kind: "Secret",
Name: "sh.helm.release.v1." + comp.GetName() + "." + getReleaseVersion(svc, comp),
Namespace: comp.GetInstanceNamespace(),
FieldPath: "data[release]",
},
ToConnectionSecretKey: "release",
SkipPartOfReleaseCheck: true,
},
}
rel, err := common.NewRelease(ctx, svc, comp, values, cd...)
rel.Spec.ForProvider.Chart.Name = comp.GetServiceName() + "-galera"

return rel, err
}

func getReleaseVersion(svc *runtime.ServiceRuntime, comp *vshnv1.VSHNMariaDB) string {
r := &xhelmbeta1.Release{}
err := svc.GetObservedComposedResource(r, comp.GetName()+"-release")
if err != nil {
return "v1"
} else {
return "v" + strconv.Itoa(r.Status.AtProvider.Revision)
}
}

// createUserSettingsConfigMap create a config map from the custom customer MariaDB settings
func createUserSettingsConfigMap(svc *runtime.ServiceRuntime, comp *vshnv1.VSHNMariaDB) error {
cm := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: comp.GetName() + "-extra-mariadb-settings",
Namespace: comp.Status.InstanceNamespace,
},
Data: map[string]string{
extraConfig: comp.Spec.Parameters.Service.MariadbSettings,
},
}

return svc.SetDesiredKubeObject(cm, comp.GetName()+"-extra-mariadb-settings")
}

// manageCustomDBSettings will manage custom MariaDB Settings set by the user.
// User settings cannot override the existing MariaDB settings for Galera but can complement it.
// To manage the MariaDB settings first we observe the release secret which has the Helm default settings for Galera.
// Then we explicitly set the default settings with the custom settings provided by the customer via include directive
func manageCustomDBSettings(svc *runtime.ServiceRuntime, comp *vshnv1.VSHNMariaDB, rSecretValues []byte) error {
if err := createUserSettingsConfigMap(svc, comp); err != nil {
return fmt.Errorf("cannot create mariadb user settings config map: %w", err)
}
decodedSecretValues, err := base64.StdEncoding.DecodeString(string(rSecretValues))
if err != nil {
return fmt.Errorf("cannot decode release secret: %w", err)
}
config, err := unzip(string(decodedSecretValues))
if err != nil {
return fmt.Errorf("cannot unzip release secret value: %w", err)
}
settings, err := extractDefaultSettings(config)
if err != nil {
return fmt.Errorf("cannot extract current default mariadb settings from release: %w", err)
}

adjustedSettings := adjustSettings(settings)

r := &xhelmbeta1.Release{}
err = svc.GetDesiredComposedResourceByName(r, comp.GetName()+"-release")
if err != nil {
return fmt.Errorf("cannot get release from function: %w", err)
}

var values map[string]interface{}
err = json.Unmarshal(r.Spec.ForProvider.Values.Raw, &values)
if err != nil {
return fmt.Errorf("cannot unmarshall release values: %w", err)
}

// reset default settings with include directive
err = unstructured.SetNestedField(values, adjustedSettings, "mariadbConfiguration")
if err != nil {
return fmt.Errorf("cannot set updated mariadb settings: %w", err)
}
// set extra volume for custom user settings
vm := []interface{}{
corev1.VolumeMount{
Name: "extra-conf",
MountPath: "/bitnami/conf/" + extraConfig,
SubPath: extraConfig,
},
}
err = common.SetNestedObjectValue(values, []string{"extraVolumeMounts"}, vm)
if err != nil {
return fmt.Errorf("cannot set extraVolumeMounts in release: %w", err)
}

// configure extra volume for custom user settings
v := []interface{}{
corev1.Volume{
Name: "extra-conf",
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: comp.GetName() + "-extra-mariadb-settings",
},
DefaultMode: ptr.To(int32(420)),
},
},
},
}
err = common.SetNestedObjectValue(values, []string{"extraVolumes"}, v)
if err != nil {
return fmt.Errorf("cannot set extraVolumes in release: %w", err)
}

marshalledValues, err := json.Marshal(&values)
if err != nil {
return fmt.Errorf("cannot marshall release values: %w", err)
}
r.Spec.ForProvider.Values.Raw = marshalledValues

err = svc.SetDesiredComposedResourceWithName(r, comp.Name+"-release")
if err != nil {
return fmt.Errorf("cannot set desired release: %w", err)
}
return nil
}

// adjustSettings adds the extra configuration file to the default configuration file
func adjustSettings(settings string) string {
return settings + "\n" + "!include /bitnami/conf/" + extraConfig
}

// extractDefaultSettings get the default settings of the Helm Chart
func extractDefaultSettings(releaseConfig string) (string, error) {
var releaseConfigObj map[string]interface{}
if err := json.Unmarshal([]byte(releaseConfig), &releaseConfigObj); err != nil {
return "", fmt.Errorf("cannot unmarshall release config: %w", err)
}

defaultMariadbSettings, _, err := unstructured.NestedString(releaseConfigObj, "chart", "values", "mariadbConfiguration")
if err != nil {
return "", fmt.Errorf("cannot extract default mariadb settings: %w", err)
}

return defaultMariadbSettings, nil
}

func unzip(source string) (string, error) {
reader := bytes.NewReader([]byte(source))
gzreader, err := gzip.NewReader(reader)
if err != nil {
return "", fmt.Errorf("cannot unzip source: %w", err)
}
output, err := io.ReadAll(gzreader)
if err != nil {
return "", fmt.Errorf("cannot unzip source: %w", err)
}
return string(output), nil
}

// getReleaseSecretValue will fetch the release default helm chart configuration from connection secret
func getReleaseSecretValue(svc *runtime.ServiceRuntime, comp *vshnv1.VSHNMariaDB) []byte {
m, err := svc.GetObservedComposedResourceConnectionDetails(comp.GetName() + "-release")
if err != nil {
// The release is not yet deploy, wait for the next reconciliation
return []byte{}
}
return m["release"]
}
2 changes: 1 addition & 1 deletion pkg/comp-functions/runtime/function_mgr.go
Original file line number Diff line number Diff line change
Expand Up @@ -535,7 +535,7 @@ func (s *ServiceRuntime) GetObservedComposedResourceConnectionDetails(objectName
return object.ConnectionDetails, nil
}

// GetObservedComposedResource returns and unmarshals the observed object into the given managed resource.
// GetObservedComposedResource returns and unmarshalls the observed object into the given managed resource.
func (s *ServiceRuntime) GetObservedComposedResource(obj xpresource.Managed, name string) error {
resources, err := request.GetObservedComposedResources(s.req)
if err != nil {
Expand Down

0 comments on commit 8a1fea4

Please sign in to comment.