Files
mq-container/internal/ha/ha_test.go
2024-10-28 23:04:48 +01:00

546 lines
16 KiB
Go

/*
© Copyright IBM Corporation 2024
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package ha
import (
"bytes"
"embed"
"fmt"
"os"
"path"
"strings"
"testing"
"github.com/ibm-messaging/mq-container/pkg/logger"
)
//go:embed test_fixtures
var testFixtures embed.FS
func TestConfigFromEnv(t *testing.T) {
tests := []struct {
TestName string
env map[string]string
overrides testOverrides
expected haConfig
}{
{
TestName: "Minimal config",
env: map[string]string{
"HOSTNAME": "minimal-config",
"MQ_NATIVE_HA_INSTANCE_0_NAME": "minimal-config-instance0",
"MQ_NATIVE_HA_INSTANCE_1_NAME": "minimal-config-instance1",
"MQ_NATIVE_HA_INSTANCE_2_NAME": "minimal-config-instance2",
"MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS": "minimal-config-instance0(9145)",
"MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS": "minimal-config-instance1(9145)",
"MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS": "minimal-config-instance2(9145)",
},
expected: haConfig{
Name: "minimal-config",
Instances: [3]haInstance{
{"minimal-config-instance0", "minimal-config-instance0(9145)"},
{"minimal-config-instance1", "minimal-config-instance1(9145)"},
{"minimal-config-instance2", "minimal-config-instance2(9145)"},
},
},
},
{
TestName: "Full TLS config",
env: map[string]string{
"HOSTNAME": "tls-config",
"MQ_NATIVE_HA_INSTANCE_0_NAME": "tls-config-instance0",
"MQ_NATIVE_HA_INSTANCE_1_NAME": "tls-config-instance1",
"MQ_NATIVE_HA_INSTANCE_2_NAME": "tls-config-instance2",
"MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS": "tls-config-instance0(9145)",
"MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS": "tls-config-instance1(9145)",
"MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS": "tls-config-instance2(9145)",
"MQ_NATIVE_HA_TLS": "true",
"MQ_NATIVE_HA_CIPHERSPEC": "a-cipher-spec",
"MQ_NATIVE_HA_KEY_REPOSITORY": "/path/to/repository",
},
overrides: testOverrides{
certificateLabel: asRef("cert-label-here"),
fips: asRef(false),
},
expected: haConfig{
Name: "tls-config",
Instances: [3]haInstance{
{"tls-config-instance0", "tls-config-instance0(9145)"},
{"tls-config-instance1", "tls-config-instance1(9145)"},
{"tls-config-instance2", "tls-config-instance2(9145)"},
},
haTLSEnabled: true,
CipherSpec: "a-cipher-spec",
keyRepository: "/path/to/repository",
CertificateLabel: "cert-label-here", // From override
fipsAvailable: false, // From override
},
},
{
TestName: "Group TLS (live plain) config",
env: map[string]string{
"HOSTNAME": "group-live-plain-config",
"MQ_NATIVE_HA_INSTANCE_0_NAME": "group-live-plain-config0",
"MQ_NATIVE_HA_INSTANCE_1_NAME": "group-live-plain-config1",
"MQ_NATIVE_HA_INSTANCE_2_NAME": "group-live-plain-config2",
"MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS": "group-live-plain-config0(9145)",
"MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS": "group-live-plain-config1(9145)",
"MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS": "group-live-plain-config2(9145)",
"MQ_NATIVE_HA_CIPHERSPEC": "NULL",
"MQ_NATIVE_HA_KEY_REPOSITORY": "/path/to/repository",
"MQ_NATIVE_HA_GROUP_RECOVERY_ENABLED": "true",
"MQ_NATIVE_HA_GROUP_LOCAL_NAME": "alpha",
"MQ_NATIVE_HA_GROUP_RECOVERY_NAME": "beta",
"MQ_NATIVE_HA_GROUP_CIPHERSPEC": "ANY_TLS",
"MQ_NATIVE_HA_GROUP_ROLE": "Live",
"MQ_NATIVE_HA_GROUP_LOCAL_ADDRESS": "(4445)",
"MQ_NATIVE_HA_GROUP_REPLICATION_ADDRESS": "beta-address(4445)",
},
overrides: testOverrides{
groupCertificateLabel: asRef("recovery-cert-label-here"),
fips: asRef(false),
},
expected: haConfig{
Name: "group-live-plain-config",
Instances: [3]haInstance{
{"group-live-plain-config0", "group-live-plain-config0(9145)"},
{"group-live-plain-config1", "group-live-plain-config1(9145)"},
{"group-live-plain-config2", "group-live-plain-config2(9145)"},
},
Group: haGroupConfig{
Local: haLocalGroupConfig{
Name: "alpha",
Role: "Live",
Address: "(4445)",
},
Recovery: haRecoveryGroupConfig{
Name: "beta",
Enabled: true,
Address: "beta-address(4445)",
},
CertificateLabel: "recovery-cert-label-here", // From override
CipherSpec: "ANY_TLS",
},
CipherSpec: "NULL",
keyRepository: "/path/to/repository",
haTLSEnabled: true,
fipsAvailable: false, // From override
},
},
}
for _, test := range tests {
t.Run(test.TestName, func(t *testing.T) {
// Set environment for test
savedEnv := make([]string, len(os.Environ()))
copy(savedEnv, os.Environ())
defer func() {
os.Clearenv()
for _, env := range savedEnv {
parts := strings.SplitN(env, "=", 2)
os.Setenv(parts[0], parts[1])
}
}()
for key, value := range test.env {
os.Setenv(key, value)
}
testLogger, logBuffer, err := newTestLogger(test.TestName)
if err != nil {
t.Fatalf("Failed to create test logger: %s", err.Error())
}
if !envConfigPresent() {
t.Fatalf("Check for Native HA config by environment variable unexpectedly reported false")
}
// Load config from env
cfg, err := loadConfigFromEnv(testLogger)
t.Log(logBuffer.String())
if err != nil {
t.Fatalf("Loading config failed: %s", err.Error())
}
test.overrides.apply(cfg)
// Validate
if *cfg != test.expected {
t.Fatalf("Configuration does not match expected:\n\tExpected: %#v\n\tActual: %#v\n", test.expected, *cfg)
}
})
}
}
func TestCheckEnv(t *testing.T) {
tests := []struct {
name string
env map[string]string
expect bool
}{
{
name: "empty env",
expect: false,
},
{
name: "Native HA with external config",
env: map[string]string{
"HOSTNAME": "external-config",
"MQ_NATIVE_HA": "true",
},
expect: false,
},
{
name: "Native HA with env config",
env: map[string]string{
"MQ_NATIVE_HA": "true",
"MQ_NATIVE_HA_INSTANCE_0_NAME": "minimal-config-instance0",
"MQ_NATIVE_HA_INSTANCE_1_NAME": "minimal-config-instance1",
"MQ_NATIVE_HA_INSTANCE_2_NAME": "minimal-config-instance2",
"MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS": "minimal-config-instance0(9145)",
"MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS": "minimal-config-instance1(9145)",
"MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS": "minimal-config-instance2(9145)",
},
expect: true,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
// Set environment for test
savedEnv := make([]string, len(os.Environ()))
copy(savedEnv, os.Environ())
defer func() {
os.Clearenv()
for _, env := range savedEnv {
parts := strings.SplitN(env, "=", 2)
os.Setenv(parts[0], parts[1])
}
}()
for key, value := range test.env {
os.Setenv(key, value)
}
actual := envConfigPresent()
if actual != test.expect {
t.Fatalf("Incorrect result from environment variable check (actual: %v != expected: %v)", actual, test.expect)
}
})
}
}
func TestTemplatingFromConfig(t *testing.T) {
// Helper function to turn pairs of strings into a map
templateListToMap := func(paths ...string) map[string]string {
templates := map[string]string{}
for i := 0; i+1 < len(paths); i += 2 {
input := path.Join("../../ha/", paths[i])
output := paths[i+1]
templates[input] = output
}
return templates
}
tests := []struct {
TestName string
config haConfig
templates map[string]string
}{
{
TestName: "MinimalConfig (no-FIPS)",
config: haConfig{
fipsAvailable: false,
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha.ini.tpl", "envcfg/minimal-config.ini",
),
}, {
TestName: "MinimalConfig (FIPS)",
config: haConfig{
fipsAvailable: true,
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/fips.ini",
"10-native-ha.ini.tpl", "envcfg/minimal-config.ini",
),
},
{
TestName: "Base TLS config (no FIPS)",
config: haConfig{
haTLSEnabled: true,
CertificateLabel: "baseTLS",
fipsAvailable: false,
},
templates: templateListToMap(
"10-native-ha.ini.tpl", "envcfg/minimal-config.ini",
),
},
{
TestName: "Base TLS config (with FIPS)",
config: haConfig{
haTLSEnabled: true,
CertificateLabel: "baseTLS",
fipsAvailable: true,
},
templates: templateListToMap(
"10-native-ha-keystore.ini.tpl", "keystore/ha-only.ini",
"10-native-ha.ini.tpl", "envcfg/minimal-config.ini",
),
},
{
TestName: "Full TLS config (no-fips)",
config: haConfig{
haTLSEnabled: true,
CipherSpec: "some-cipher",
keyRepository: "/an/overridden/keystore",
fipsAvailable: false,
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha-keystore.ini.tpl", "keystore/overridden-path.ini",
"10-native-ha.ini.tpl", "envcfg/tls-full.ini",
),
},
{
TestName: "Minimal live config",
config: haConfig{
Group: haGroupConfig{
Local: haLocalGroupConfig{
Name: "alpha",
Role: "Live",
},
Recovery: haRecoveryGroupConfig{
Name: "beta",
Enabled: true,
Address: "beta-address(4445)",
},
CertificateLabel: "recoveryTLS",
},
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha-keystore.ini.tpl", "keystore/group-only.ini",
"10-native-ha.ini.tpl", "envcfg/group-live-minimal.ini",
),
},
{
TestName: "Minimal recovery config",
config: haConfig{
Group: haGroupConfig{
Local: haLocalGroupConfig{
Name: "beta",
Role: "Recovery",
},
Recovery: haRecoveryGroupConfig{
Name: "alpha",
Enabled: true,
Address: "alpha-address(4445)",
},
CertificateLabel: "recoveryTLS",
},
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha-keystore.ini.tpl", "keystore/group-only.ini",
"10-native-ha.ini.tpl", "envcfg/group-recovery-minimal.ini",
),
},
{
TestName: "Group TLS (live plain) config",
config: haConfig{
Group: haGroupConfig{
Local: haLocalGroupConfig{
Name: "alpha",
Role: "Live",
Address: "(4445)",
},
Recovery: haRecoveryGroupConfig{
Name: "beta",
Enabled: true,
Address: "beta-address(4445)",
},
CertificateLabel: "recoveryTLS",
CipherSpec: "ANY_TLS",
},
CipherSpec: "NULL",
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha-keystore.ini.tpl", "keystore/group-only.ini",
"10-native-ha.ini.tpl", "envcfg/group-live-plain-ha.ini",
),
},
{
TestName: "Separate HA and Group TLS config",
config: haConfig{
Group: haGroupConfig{
Local: haLocalGroupConfig{
Name: "alpha",
Role: "Live",
Address: "(4445)",
},
Recovery: haRecoveryGroupConfig{
Name: "beta",
Enabled: true,
Address: "beta-address(4445)",
},
CertificateLabel: "recoveryTLS",
CipherSpec: "ANY_TLS",
},
CertificateLabel: "baseTLS",
CipherSpec: "NULL",
},
templates: templateListToMap(
"10-native-ha-instance.ini.tpl", "instance/no-fips.ini",
"10-native-ha-keystore.ini.tpl", "keystore/ha-group.ini",
"10-native-ha.ini.tpl", "envcfg/group-live-plain-ha.ini",
),
},
}
for _, test := range tests {
t.Run(test.TestName, func(t *testing.T) {
for templateFile, expectedFile := range test.templates {
t.Run(templateFile, func(t *testing.T) {
t.Logf(`Runing templating test "%s"`, test.TestName)
t.Logf(`Expected to match template "%s"`, expectedFile)
testLogger, logBuffer, err := newTestLogger(test.TestName)
if err != nil {
t.Fatalf("Failed to create test logger: %s", err.Error())
}
// Load test config
cfg := applyTestDefaults(test.config)
// Generate template
tempOutputPath, err := os.CreateTemp("", "")
if err != nil {
t.Fatalf("Failed to create temporary output file: %s", err.Error())
}
defer func() { _ = os.Remove(tempOutputPath.Name()) }()
err = cfg.generate(templateFile, tempOutputPath.Name(), testLogger)
t.Log(logBuffer.String())
if err != nil {
t.Fatalf("Processing template to config failed: %s", err.Error())
}
actual, err := os.ReadFile(tempOutputPath.Name())
if err != nil {
t.Fatalf("Failed to read '%s': %s", test.TestName, err.Error())
}
// Validate
assertIniMatch(t, string(actual), expectedFile)
})
}
})
}
}
func applyTestDefaults(testConfig haConfig) haConfig {
baseName := "test-config"
setIfBlank(&testConfig.Name, baseName)
for i := 0; i < 3; i++ {
instName := fmt.Sprintf("%s-instance%d", baseName, i)
replAddress := fmt.Sprintf("%s(9145)", instName)
setIfBlank(&testConfig.Instances[i].Name, instName)
setIfBlank(&testConfig.Instances[i].ReplicationAddress, replAddress)
}
return testConfig
}
func setIfBlank[T comparable](setting *T, val T) {
var zero T
if *setting == zero {
*setting = val
}
}
func assertIniMatch(t *testing.T, actual string, expectedResultName string) {
expectedContent, err := testFixtures.ReadFile(fmt.Sprintf("test_fixtures/%s", expectedResultName))
if err != nil {
t.Fatalf("Failed to read expected results file (%s): %s", expectedResultName, err.Error())
}
expectedLines := strings.Split(string(expectedContent), "\n")
actualLines := strings.Split(actual, "\n")
filterBlank := func(lines *[]string) {
n := 0
for i := 0; i < len(*lines); i++ {
if strings.TrimSpace((*lines)[i]) == "" {
continue
}
(*lines)[n] = (*lines)[i]
n++
}
*lines = (*lines)[0:n]
}
filterBlank(&expectedLines)
filterBlank(&actualLines)
maxLine := len(expectedLines)
if len(actualLines) > maxLine {
maxLine = len(actualLines)
}
for i := 0; i < maxLine; i++ {
actLine, expLine := "", ""
if i < len(actualLines) {
actLine = actualLines[i]
}
if i < len(expectedLines) {
expLine = expectedLines[i]
}
if actLine != expLine {
t.Fatalf("Template does not match\n\nFirst difference at line %d:\n\tExpected: %s\n\tActual : %s\n\nExpected:\n\t%s\n\nActual:\n\t%s", i+1, expLine, actLine, strings.Join(expectedLines, "\n\t"), strings.Join(actualLines, "\n\t"))
}
}
}
func newTestLogger(name string) (*logger.Logger, *bytes.Buffer, error) {
buffer := new(bytes.Buffer)
l, err := logger.NewLogger(buffer, true, false, name)
return l, buffer, err
}
type testOverrides struct {
certificateLabel *string
groupCertificateLabel *string
fips *bool
}
func (t testOverrides) apply(cfg *haConfig) {
if t.certificateLabel != nil {
cfg.CertificateLabel = *t.certificateLabel
cfg.haTLSEnabled = true
}
if t.groupCertificateLabel != nil {
cfg.Group.CertificateLabel = *t.groupCertificateLabel
cfg.haTLSEnabled = true
}
if t.fips != nil {
cfg.fipsAvailable = *t.fips
}
}
func asRef[T any](val T) *T {
ref := &val
return ref
}