546 lines
16 KiB
Go
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
|
|
}
|