first commit
This commit is contained in:
44
internal/command/command.go
Normal file
44
internal/command/command.go
Normal file
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2017, 2022
|
||||
|
||||
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 command contains code to run external commands
|
||||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
// Run runs an OS command. On Linux it waits for the command to
|
||||
// complete and returns the exit status (return code).
|
||||
// Do not use this function to run shell built-ins (like "cd"), because
|
||||
// the error handling works differently
|
||||
func Run(name string, arg ...string) (string, int, error) {
|
||||
return RunContext(context.Background(), name, arg...)
|
||||
}
|
||||
|
||||
func RunContext(ctx context.Context, name string, arg ...string) (string, int, error) {
|
||||
// Run the command and wait for completion
|
||||
// #nosec G204
|
||||
cmd := exec.CommandContext(ctx, name, arg...)
|
||||
out, err := cmd.CombinedOutput()
|
||||
rc := cmd.ProcessState.ExitCode()
|
||||
if err != nil {
|
||||
return string(out), rc, fmt.Errorf("%v: %v", cmd.Path, err)
|
||||
}
|
||||
return string(out), rc, nil
|
||||
}
|
||||
47
internal/command/command_test.go
Normal file
47
internal/command/command_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2017
|
||||
|
||||
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 command
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var commandTests = []struct {
|
||||
name string
|
||||
arg []string
|
||||
rc int
|
||||
}{
|
||||
{"ls", []string{}, 0},
|
||||
{"ls", []string{"madeup"}, 2},
|
||||
{"bash", []string{"-c", "exit 99"}, 99},
|
||||
}
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
if runtime.GOOS != "linux" {
|
||||
t.Skip("Skipping tests for package which only works on Linux")
|
||||
}
|
||||
for _, table := range commandTests {
|
||||
arg := table.arg
|
||||
_, rc, err := Run(table.name, arg...)
|
||||
if rc != table.rc {
|
||||
t.Errorf("Run(%v,%v) - expected %v, got %v", table.name, table.arg, table.rc, rc)
|
||||
}
|
||||
if rc != 0 && err == nil {
|
||||
t.Errorf("Run(%v,%v) - expected error for non-zero return code (rc=%v)", table.name, table.arg, rc)
|
||||
}
|
||||
}
|
||||
}
|
||||
287
internal/containerruntime/amicontained.go
Normal file
287
internal/containerruntime/amicontained.go
Normal file
@@ -0,0 +1,287 @@
|
||||
/*
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018 Jessica Frazelle
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
*/
|
||||
|
||||
/*
|
||||
The code for amicontained.go is forked from
|
||||
https://github.com/genuinetools/bpfd/blob/434b609b3d4a5aeb461109b1167b68e000b72f69/proc/proc.go
|
||||
|
||||
The code was forked when the latest details are as "Latest commit 871fc34 on Sep 18, 2018"
|
||||
|
||||
*/
|
||||
|
||||
// Adding IBM Copyright since the forked code had to be modified to remove deprecated ioutil package
|
||||
/*
|
||||
© Copyright IBM Corporation 2023
|
||||
*/
|
||||
|
||||
package containerruntime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/syndtr/gocapability/capability"
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// ContainerRuntime is the type for the various container runtime strings.
|
||||
type ContainerRuntime string
|
||||
|
||||
// SeccompMode is the type for the various seccomp mode strings.
|
||||
type SeccompMode string
|
||||
|
||||
const (
|
||||
// RuntimeDocker is the string for the docker runtime.
|
||||
RuntimeDocker ContainerRuntime = "docker"
|
||||
// RuntimeRkt is the string for the rkt runtime.
|
||||
RuntimeRkt ContainerRuntime = "rkt"
|
||||
// RuntimeNspawn is the string for the systemd-nspawn runtime.
|
||||
RuntimeNspawn ContainerRuntime = "systemd-nspawn"
|
||||
// RuntimeLXC is the string for the lxc runtime.
|
||||
RuntimeLXC ContainerRuntime = "lxc"
|
||||
// RuntimeLXCLibvirt is the string for the lxc-libvirt runtime.
|
||||
RuntimeLXCLibvirt ContainerRuntime = "lxc-libvirt"
|
||||
// RuntimeOpenVZ is the string for the openvz runtime.
|
||||
RuntimeOpenVZ ContainerRuntime = "openvz"
|
||||
// RuntimeKubernetes is the string for the kubernetes runtime.
|
||||
RuntimeKubernetes ContainerRuntime = "kube"
|
||||
// RuntimeGarden is the string for the garden runtime.
|
||||
RuntimeGarden ContainerRuntime = "garden"
|
||||
// RuntimePodman is the string for the podman runtime.
|
||||
RuntimePodman ContainerRuntime = "podman"
|
||||
// RuntimeNotFound is the string for when no container runtime is found.
|
||||
RuntimeNotFound ContainerRuntime = "not-found"
|
||||
|
||||
// SeccompModeDisabled is equivalent to "0" in the /proc/{pid}/status file.
|
||||
SeccompModeDisabled SeccompMode = "disabled"
|
||||
// SeccompModeStrict is equivalent to "1" in the /proc/{pid}/status file.
|
||||
SeccompModeStrict SeccompMode = "strict"
|
||||
// SeccompModeFiltering is equivalent to "2" in the /proc/{pid}/status file.
|
||||
SeccompModeFiltering SeccompMode = "filtering"
|
||||
|
||||
apparmorUnconfined = "unconfined"
|
||||
|
||||
uint32Max = 4294967295
|
||||
|
||||
statusFileValue = ":(.*)"
|
||||
)
|
||||
|
||||
var (
|
||||
// ContainerRuntimes contains all the container runtimes.
|
||||
ContainerRuntimes = []ContainerRuntime{
|
||||
RuntimeDocker,
|
||||
RuntimeRkt,
|
||||
RuntimeNspawn,
|
||||
RuntimeLXC,
|
||||
RuntimeLXCLibvirt,
|
||||
RuntimeOpenVZ,
|
||||
RuntimeKubernetes,
|
||||
RuntimeGarden,
|
||||
RuntimePodman,
|
||||
}
|
||||
|
||||
seccompModes = map[string]SeccompMode{
|
||||
"0": SeccompModeDisabled,
|
||||
"1": SeccompModeStrict,
|
||||
"2": SeccompModeFiltering,
|
||||
}
|
||||
|
||||
statusFileValueRegex = regexp.MustCompile(statusFileValue)
|
||||
)
|
||||
|
||||
// GetContainerRuntime returns the container runtime the process is running in.
|
||||
// If pid is less than one, it returns the runtime for "self".
|
||||
func GetContainerRuntime(tgid, pid int) ContainerRuntime {
|
||||
file := "/proc/self/cgroup"
|
||||
if pid > 0 {
|
||||
if tgid > 0 {
|
||||
file = fmt.Sprintf("/proc/%d/task/%d/cgroup", tgid, pid)
|
||||
} else {
|
||||
file = fmt.Sprintf("/proc/%d/cgroup", pid)
|
||||
}
|
||||
}
|
||||
|
||||
// read the cgroups file
|
||||
a := readFileString(file)
|
||||
runtime := getContainerRuntime(a)
|
||||
if runtime != RuntimeNotFound {
|
||||
return runtime
|
||||
}
|
||||
|
||||
// /proc/vz exists in container and outside of the container, /proc/bc only outside of the container.
|
||||
if fileExists("/proc/vz") && !fileExists("/proc/bc") {
|
||||
return RuntimeOpenVZ
|
||||
}
|
||||
|
||||
a = os.Getenv("container")
|
||||
runtime = getContainerRuntime(a)
|
||||
if runtime != RuntimeNotFound {
|
||||
return runtime
|
||||
}
|
||||
|
||||
// PID 1 might have dropped this information into a file in /run.
|
||||
// Read from /run/systemd/container since it is better than accessing /proc/1/environ,
|
||||
// which needs CAP_SYS_PTRACE
|
||||
a = readFileString("/run/systemd/container")
|
||||
runtime = getContainerRuntime(a)
|
||||
if runtime != RuntimeNotFound {
|
||||
return runtime
|
||||
}
|
||||
|
||||
return RuntimeNotFound
|
||||
}
|
||||
|
||||
func getContainerRuntime(input string) ContainerRuntime {
|
||||
if len(strings.TrimSpace(input)) < 1 {
|
||||
return RuntimeNotFound
|
||||
}
|
||||
|
||||
for _, runtime := range ContainerRuntimes {
|
||||
if strings.Contains(input, string(runtime)) {
|
||||
return runtime
|
||||
}
|
||||
}
|
||||
|
||||
return RuntimeNotFound
|
||||
}
|
||||
|
||||
func fileExists(file string) bool {
|
||||
if _, err := os.Stat(file); !os.IsNotExist(err) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func readFile(file string) []byte {
|
||||
if !fileExists(file) {
|
||||
return nil
|
||||
}
|
||||
// filepath.clean was added to resolve the gosec build failure
|
||||
// with error "Potential file inclusion via variable"
|
||||
// IBM Modified the below line to remove the deprecated ioutil dependency
|
||||
b, err := os.ReadFile(filepath.Clean(file))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// GetCapabilities returns the allowed capabilities for the process.
|
||||
// If pid is less than one, it returns the capabilities for "self".
|
||||
func GetCapabilities(pid int) (map[string][]string, error) {
|
||||
allCaps := capability.List()
|
||||
|
||||
caps, err := capability.NewPid(pid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allowedCaps := map[string][]string{}
|
||||
allowedCaps["EFFECTIVE | PERMITTED | INHERITABLE"] = []string{}
|
||||
allowedCaps["BOUNDING"] = []string{}
|
||||
allowedCaps["AMBIENT"] = []string{}
|
||||
|
||||
for _, cap := range allCaps {
|
||||
if caps.Get(capability.CAPS, cap) {
|
||||
allowedCaps["EFFECTIVE | PERMITTED | INHERITABLE"] = append(allowedCaps["EFFECTIVE | PERMITTED | INHERITABLE"], cap.String())
|
||||
}
|
||||
if caps.Get(capability.BOUNDING, cap) {
|
||||
allowedCaps["BOUNDING"] = append(allowedCaps["BOUNDING"], cap.String())
|
||||
}
|
||||
if caps.Get(capability.AMBIENT, cap) {
|
||||
allowedCaps["AMBIENT"] = append(allowedCaps["AMBIENT"], cap.String())
|
||||
}
|
||||
}
|
||||
|
||||
return allowedCaps, nil
|
||||
}
|
||||
|
||||
// GetSeccompEnforcingMode returns the seccomp enforcing level (disabled, filtering, strict)
|
||||
// for a process.
|
||||
// If pid is less than one, it returns the seccomp enforcing mode for "self".
|
||||
func GetSeccompEnforcingMode(pid int) SeccompMode {
|
||||
file := "/proc/self/status"
|
||||
if pid > 0 {
|
||||
file = fmt.Sprintf("/proc/%d/status", pid)
|
||||
}
|
||||
|
||||
return getSeccompEnforcingMode(readFileString(file))
|
||||
}
|
||||
|
||||
func getSeccompEnforcingMode(input string) SeccompMode {
|
||||
mode := getStatusEntry(input, "Seccomp:")
|
||||
sm, ok := seccompModes[mode]
|
||||
if ok {
|
||||
return sm
|
||||
}
|
||||
|
||||
// Pre linux 3.8, check if Seccomp is supported, via CONFIG_SECCOMP.
|
||||
if err := unix.Prctl(unix.PR_GET_SECCOMP, 0, 0, 0, 0); err != unix.EINVAL {
|
||||
// Make sure the kernel has CONFIG_SECCOMP_FILTER.
|
||||
if err := unix.Prctl(unix.PR_SET_SECCOMP, unix.SECCOMP_MODE_FILTER, 0, 0, 0); err != unix.EINVAL {
|
||||
return SeccompModeStrict
|
||||
}
|
||||
}
|
||||
|
||||
return SeccompModeDisabled
|
||||
}
|
||||
|
||||
// TODO: make this function more efficient and read the file line by line.
|
||||
func getStatusEntry(input, find string) string {
|
||||
// Split status file string by line
|
||||
statusMappings := strings.Split(input, "\n")
|
||||
statusMappings = deleteEmpty(statusMappings)
|
||||
|
||||
for _, line := range statusMappings {
|
||||
if strings.Contains(line, find) {
|
||||
matches := statusFileValueRegex.FindStringSubmatch(line)
|
||||
if len(matches) > 1 {
|
||||
return strings.TrimSpace(matches[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func deleteEmpty(s []string) []string {
|
||||
var r []string
|
||||
for _, str := range s {
|
||||
if strings.TrimSpace(str) != "" {
|
||||
r = append(r, strings.TrimSpace(str))
|
||||
}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func readFileString(file string) string {
|
||||
b := readFile(file)
|
||||
if b == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(string(b))
|
||||
}
|
||||
124
internal/containerruntime/runtime.go
Normal file
124
internal/containerruntime/runtime.go
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2019,2023
|
||||
|
||||
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 containerruntime
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func DetectContainerRuntime() ContainerRuntime {
|
||||
return GetContainerRuntime(0, 1)
|
||||
}
|
||||
|
||||
func GetBaseImage() (string, error) {
|
||||
buf, err := os.ReadFile("/etc/os-release")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to read /etc/os-release: %v", err)
|
||||
}
|
||||
lines := strings.Split(string(buf), "\n")
|
||||
for _, l := range lines {
|
||||
if strings.HasPrefix(l, "PRETTY_NAME=") {
|
||||
words := strings.Split(l, "\"")
|
||||
if len(words) >= 2 {
|
||||
return words[1], nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "unknown", nil
|
||||
}
|
||||
|
||||
// GetCapabilities gets the Linux capabilities (e.g. setuid, setgid). See https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities
|
||||
func GetContainerCapabilities() (map[string][]string, error) {
|
||||
//passing the pid as 1 since runmqserver initializes, creates and starts a queue manager, as PID 1 in a container
|
||||
return GetCapabilities(1)
|
||||
}
|
||||
|
||||
// GetSeccomp gets the seccomp enforcing mode, which affects which kernel calls can be made
|
||||
func GetSeccomp() SeccompMode {
|
||||
//passing the pid as 1 since runmqserver initializes, creates and starts a queue manager, as PID 1 in a container
|
||||
return GetSeccompEnforcingMode(1)
|
||||
}
|
||||
|
||||
// GetSecurityAttributes gets the security attributes of the current process.
|
||||
// The security attributes indicate whether AppArmor or SELinux are being used,
|
||||
// and what the level of confinement is.
|
||||
func GetSecurityAttributes() string {
|
||||
a, err := readProc("/proc/self/attr/current")
|
||||
// On some systems, if AppArmor or SELinux are not installed, you get an
|
||||
// error when you try and read `/proc/self/attr/current`, even though the
|
||||
// file exists.
|
||||
if err != nil || a == "" {
|
||||
a = "none"
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func readProc(filename string) (value string, err error) {
|
||||
// #nosec G304
|
||||
buf, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(string(buf)), nil
|
||||
}
|
||||
|
||||
func GetMounts() (map[string]string, error) {
|
||||
all, err := readProc("/proc/mounts")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Couldn't read /proc/mounts")
|
||||
}
|
||||
result := make(map[string]string)
|
||||
lines := strings.Split(all, "\n")
|
||||
for i := range lines {
|
||||
parts := strings.Split(lines[i], " ")
|
||||
//dev := parts[0]
|
||||
mountPoint := parts[1]
|
||||
fsType := parts[2]
|
||||
if strings.Contains(mountPoint, "/mnt/mqm") {
|
||||
result[mountPoint] = fsType
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func GetKernelVersion() (string, error) {
|
||||
return readProc("/proc/sys/kernel/osrelease")
|
||||
}
|
||||
|
||||
func GetMaxFileHandles() (string, error) {
|
||||
return readProc("/proc/sys/fs/file-max")
|
||||
}
|
||||
|
||||
// SupportedFilesystem returns true if the supplied filesystem type is supported for MQ data
|
||||
func SupportedFilesystem(fsType string) bool {
|
||||
switch fsType {
|
||||
case "aufs", "overlayfs", "tmpfs":
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// ValidMultiInstanceFilesystem returns true if the supplied filesystem type is valid for a multi-instance queue manager
|
||||
func ValidMultiInstanceFilesystem(fsType string) bool {
|
||||
if !SupportedFilesystem(fsType) {
|
||||
return false
|
||||
}
|
||||
// TODO : check for non-shared filesystems & shared filesystems which are known not to work
|
||||
return true
|
||||
}
|
||||
115
internal/containerruntime/runtime_linux.go
Normal file
115
internal/containerruntime/runtime_linux.go
Normal file
@@ -0,0 +1,115 @@
|
||||
// +build linux
|
||||
|
||||
/*
|
||||
© Copyright IBM Corporation 2017, 2019
|
||||
|
||||
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 containerruntime
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// fsTypes contains file system identifier codes.
|
||||
// This code will not compile on some operating systems - Linux only.
|
||||
var fsTypes = map[int64]string{
|
||||
0x61756673: "aufs",
|
||||
0xef53: "ext",
|
||||
0x6969: "nfs",
|
||||
0x65735546: "fuse",
|
||||
0x9123683e: "btrfs",
|
||||
0x01021994: "tmpfs",
|
||||
0x794c7630: "overlayfs",
|
||||
0x58465342: "xfs",
|
||||
// less popular codes
|
||||
0xadf5: "adfs",
|
||||
0xadff: "affs",
|
||||
0x5346414F: "afs",
|
||||
0x0187: "autofs",
|
||||
0x73757245: "coda",
|
||||
0x28cd3d45: "cramfs",
|
||||
0x453dcd28: "cramfs",
|
||||
0x64626720: "debugfs",
|
||||
0x73636673: "securityfs",
|
||||
0xf97cff8c: "selinux",
|
||||
0x43415d53: "smack",
|
||||
0x858458f6: "ramfs",
|
||||
0x958458f6: "hugetlbfs",
|
||||
0x73717368: "squashfs",
|
||||
0xf15f: "ecryptfs",
|
||||
0x414A53: "efs",
|
||||
0xabba1974: "xenfs",
|
||||
0x3434: "nilfs",
|
||||
0xF2F52010: "f2fs",
|
||||
0xf995e849: "hpfs",
|
||||
0x9660: "isofs",
|
||||
0x72b6: "jffs2",
|
||||
0x6165676C: "pstorefs",
|
||||
0xde5e81e4: "efivarfs",
|
||||
0x00c0ffee: "hostfs",
|
||||
0x137F: "minix_14", // minix v1 fs, 14 char names
|
||||
0x138F: "minix_30", // minix v1 fs, 30 char names
|
||||
0x2468: "minix2_14", // minix v2 fs, 14 char names
|
||||
0x2478: "minix2_30", // minix v2 fs, 30 char names
|
||||
0x4d5a: "minix3_60", // minix v3 fs, 60 char names
|
||||
0x4d44: "msdos",
|
||||
0x564c: "ncp",
|
||||
0x7461636f: "ocfs2",
|
||||
0x9fa1: "openprom",
|
||||
0x002f: "qnx4",
|
||||
0x68191122: "qnx6",
|
||||
0x6B414653: "afs_fs",
|
||||
0x52654973: "reiserfs",
|
||||
0x517B: "smb",
|
||||
0x27e0eb: "cgroup",
|
||||
0x63677270: "cgroup2",
|
||||
0x7655821: "rdtgroup",
|
||||
0x57AC6E9D: "stack_end",
|
||||
0x74726163: "tracefs",
|
||||
0x01021997: "v9fs",
|
||||
0x62646576: "bdevfs",
|
||||
0x64646178: "daxfs",
|
||||
0x42494e4d: "binfmtfs",
|
||||
0x1cd1: "devpts",
|
||||
0xBAD1DEA: "futexfs",
|
||||
0x50495045: "pipefs",
|
||||
0x9fa0: "proc",
|
||||
0x534F434B: "sockfs",
|
||||
0x62656572: "sysfs",
|
||||
0x9fa2: "usbdevice",
|
||||
0x11307854: "mtd_inode",
|
||||
0x09041934: "anon_inode",
|
||||
0x73727279: "btrfs",
|
||||
0x6e736673: "nsfs",
|
||||
0xcafe4a11: "bpf",
|
||||
0x5a3c69f0: "aafs",
|
||||
0x15013346: "udf",
|
||||
0x13661366: "balloon_kvm",
|
||||
0x58295829: "zsmalloc",
|
||||
}
|
||||
|
||||
// GetFilesystem returns the filesystem type for the specified path
|
||||
func GetFilesystem(path string) (string, error) {
|
||||
statfs := &unix.Statfs_t{}
|
||||
err := unix.Statfs(path, statfs)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
// Use a type conversion to make type an int64. On s390x it's a uint32.
|
||||
t, ok := fsTypes[int64(statfs.Type)]
|
||||
if !ok {
|
||||
return "unknown", nil
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
24
internal/containerruntime/runtime_other.go
Normal file
24
internal/containerruntime/runtime_other.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// +build !linux
|
||||
|
||||
/*
|
||||
© Copyright IBM Corporation 2018, 2019
|
||||
|
||||
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 containerruntime
|
||||
|
||||
// Dummy version of this function, only for non-Linux systems.
|
||||
// Having this allows unit tests to be run on other platforms (e.g. macOS)
|
||||
func checkFS(path string) error {
|
||||
return nil
|
||||
}
|
||||
61
internal/copy/copy.go
Normal file
61
internal/copy/copy.go
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2019
|
||||
|
||||
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 copy
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/internal/filecheck"
|
||||
)
|
||||
|
||||
func CopyFileMode(src, dest string, perm os.FileMode) error {
|
||||
|
||||
err := filecheck.CheckFileSource(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %s for copy: %v", src, err)
|
||||
}
|
||||
|
||||
// #nosec G304 - filename variable 'src' is checked above to ensure it is valid
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %s for copy: %v", src, err)
|
||||
}
|
||||
// #nosec G307 - local to this function, pose no harm.
|
||||
defer in.Close()
|
||||
|
||||
// #nosec G304 - this func creates based on the input filemode.
|
||||
out, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY, perm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open %s for copy: %v", dest, err)
|
||||
}
|
||||
// #nosec G307 - local to this function, pose no harm.
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = out.Close()
|
||||
return err
|
||||
}
|
||||
|
||||
// CopyFile copies the specified file
|
||||
func CopyFile(src, dest string) error {
|
||||
return CopyFileMode(src, dest, 0770)
|
||||
}
|
||||
39
internal/filecheck/filecheck.go
Normal file
39
internal/filecheck/filecheck.go
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2019, 2023
|
||||
|
||||
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 filecheck
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/internal/pathutils"
|
||||
)
|
||||
|
||||
// CheckFileSource checks the filename is valid
|
||||
func CheckFileSource(fileName string) error {
|
||||
|
||||
absFile, _ := filepath.Abs(fileName)
|
||||
|
||||
prefixes := []string{"bin", "boot", "dev", "lib", "lib64", "proc", "sbin", "sys"}
|
||||
for _, prefix := range prefixes {
|
||||
if strings.HasPrefix(absFile, pathutils.CleanPath("/", prefix)) {
|
||||
return fmt.Errorf("Filename resolves to invalid path '%v'", absFile)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
40
internal/filecheck/filecheck_test.go
Normal file
40
internal/filecheck/filecheck_test.go
Normal file
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2019
|
||||
|
||||
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 filecheck
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCheckFileSource(t *testing.T) {
|
||||
|
||||
invalidFilenames := []string{"/bin", "/boot", "/dev", "/lib", "/lib64", "/proc", "/sbin", "/sys", "/bin/myfile", "/boot/mydir/myfile", "/var/../dev", "/var/../lib/myfile"}
|
||||
for _, invalidFilename := range invalidFilenames {
|
||||
err := CheckFileSource(invalidFilename)
|
||||
if err == nil {
|
||||
t.Errorf("Expected to receive an error for filename '%v'", invalidFilename)
|
||||
}
|
||||
}
|
||||
|
||||
validFilenames := []string{"/var", "/mydir/dev", "/mydir/dev/myfile"}
|
||||
for _, validFilename := range validFilenames {
|
||||
err := CheckFileSource(validFilename)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error received: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
96
internal/fips/fips.go
Normal file
96
internal/fips/fips.go
Normal file
@@ -0,0 +1,96 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2023
|
||||
|
||||
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 fips
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/internal/command"
|
||||
"github.com/ibm-messaging/mq-container/pkg/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
FIPSEnabledType int
|
||||
)
|
||||
|
||||
// FIPS has been turned off either because OS is not FIPS enabled or
|
||||
// MQ_ENABLE_FIPS environment variable is set to "false"
|
||||
const FIPS_ENABLED_OFF = 0
|
||||
|
||||
// FIPS is turned ON
|
||||
const FIPS_ENABLED_ON = 1
|
||||
|
||||
// FIPS enabled at operating system level
|
||||
const FIPS_ENABLED_PLATFORM = 1
|
||||
|
||||
// FIPS enabled via environment variable
|
||||
const FIPS_ENABLED_ENV_VAR = 2
|
||||
|
||||
// Get FIPS enabled type.
|
||||
func ProcessFIPSType(logs *logger.Logger) {
|
||||
// Run "sysctl crypto.fips_enabled" command to determine if FIPS has been enabled
|
||||
// on OS.
|
||||
FIPSEnabledType = FIPS_ENABLED_OFF
|
||||
|
||||
out, _, err := command.Run("sysctl", "crypto.fips_enabled")
|
||||
if err == nil {
|
||||
// Check the output of the command for expected output
|
||||
if strings.Contains(out, "crypto.fips_enabled = 1") {
|
||||
FIPSEnabledType = FIPS_ENABLED_PLATFORM
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have been asked to override FIPS cryptography
|
||||
fipsOverride, fipsOverrideSet := os.LookupEnv("MQ_ENABLE_FIPS")
|
||||
if fipsOverrideSet {
|
||||
if strings.EqualFold(fipsOverride, "false") || strings.EqualFold(fipsOverride, "0") {
|
||||
FIPSEnabledType = FIPS_ENABLED_OFF
|
||||
} else if strings.EqualFold(fipsOverride, "true") || strings.EqualFold(fipsOverride, "1") {
|
||||
// This is the case where OS may or may not be FIPS compliant but we have been asked
|
||||
// to run MQ queue manager, web server and Native HA in FIPS mode. This case can also
|
||||
// be used when running docker tests. If FIPS is enabled on host, then don't modify
|
||||
// the original value.
|
||||
if FIPSEnabledType != FIPS_ENABLED_PLATFORM {
|
||||
FIPSEnabledType = FIPS_ENABLED_ENV_VAR
|
||||
}
|
||||
} else if strings.EqualFold(fipsOverride, "auto") {
|
||||
// This is the default case. Leave it to the OS default as determined above.
|
||||
} else {
|
||||
// We don't recognise the value specified. Log a warning and carry on.
|
||||
if logs != nil {
|
||||
logs.Printf("Invalid value '%s' was specified for MQ_ENABLE_FIPS. The value has been ignored.\n", fipsOverride)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func IsFIPSEnabled() bool {
|
||||
return FIPSEnabledType > FIPS_ENABLED_OFF
|
||||
}
|
||||
|
||||
// Log a message on the console to indicate FIPS certified
|
||||
// cryptography being used.
|
||||
func PostInit(log *logger.Logger) {
|
||||
message := "FIPS cryptography is not enabled."
|
||||
if FIPSEnabledType == FIPS_ENABLED_PLATFORM {
|
||||
message = "FIPS cryptography is enabled. FIPS cryptography setting on the host is 'true'."
|
||||
} else if FIPSEnabledType == FIPS_ENABLED_ENV_VAR {
|
||||
message = "FIPS cryptography is enabled. FIPS cryptography setting on the host is 'false'."
|
||||
}
|
||||
|
||||
log.Println(message)
|
||||
}
|
||||
65
internal/fips/fips_test.go
Normal file
65
internal/fips/fips_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2022
|
||||
|
||||
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 keystore contains code to create and update keystores
|
||||
package fips
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnableFIPSAuto(t *testing.T) {
|
||||
ProcessFIPSType(nil)
|
||||
// Test default "auto"
|
||||
fipsType := IsFIPSEnabled()
|
||||
if fipsType {
|
||||
t.Errorf("Expected FIPS OFF but got %v\n", fipsType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableFIPSTrue(t *testing.T) {
|
||||
// Test MQ_ENABLE_FIPS=true
|
||||
os.Setenv("MQ_ENABLE_FIPS", "true")
|
||||
fmt.Println(os.Getenv("MQ_ENABLE_FIPS"))
|
||||
ProcessFIPSType(nil)
|
||||
fipsType := IsFIPSEnabled()
|
||||
if !fipsType {
|
||||
t.Errorf("Expected FIPS ON but got %v\n", fipsType)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnableFIPSFalse(t *testing.T) {
|
||||
// Test MQ_ENABLE_FIPS=false
|
||||
os.Setenv("MQ_ENABLE_FIPS", "false")
|
||||
ProcessFIPSType(nil)
|
||||
fipsType := IsFIPSEnabled()
|
||||
if fipsType {
|
||||
t.Errorf("Expected FIPS OFF but got %v\n", fipsType)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestEnableFIPSInvalid(t *testing.T) {
|
||||
// Test MQ_ENABLE_FIPS with invalid value
|
||||
os.Setenv("MQ_ENABLE_FIPS", "falseOff")
|
||||
ProcessFIPSType(nil)
|
||||
fipsType := IsFIPSEnabled()
|
||||
if fipsType {
|
||||
t.Errorf("Expected FIPS OFF but got %v\n", fipsType)
|
||||
}
|
||||
}
|
||||
215
internal/ha/ha.go
Normal file
215
internal/ha/ha.go
Normal file
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2020, 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 contains code for high availability
|
||||
package ha
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/internal/fips"
|
||||
"github.com/ibm-messaging/mq-container/internal/mqtemplate"
|
||||
"github.com/ibm-messaging/mq-container/internal/tls"
|
||||
"github.com/ibm-messaging/mq-container/pkg/logger"
|
||||
)
|
||||
|
||||
// ConfigureNativeHA configures native high availability
|
||||
func ConfigureNativeHA(log *logger.Logger) error {
|
||||
if os.Getenv("MQ_NATIVE_HA") != "true" {
|
||||
return nil
|
||||
}
|
||||
fipsAvailable := fips.IsFIPSEnabled()
|
||||
|
||||
haCertLabel, haGroupCertLabel, _, _, err := tls.ConfigureHATLSKeystore()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error loading tls keys: %w", err)
|
||||
}
|
||||
|
||||
configFiles := map[string]string{
|
||||
"/run/10-native-ha-instance.ini": "/etc/mqm/10-native-ha-instance.ini.tpl",
|
||||
}
|
||||
if haCertLabel != "" || haGroupCertLabel != "" {
|
||||
configFiles["/run/10-native-ha-keystore.ini"] = "/etc/mqm/10-native-ha-keystore.ini.tpl"
|
||||
}
|
||||
if envConfigPresent() {
|
||||
log.Println("Configuring Native HA using values provided in environment variables")
|
||||
configFiles["/run/10-native-ha.ini"] = "/etc/mqm/10-native-ha.ini.tpl"
|
||||
}
|
||||
return loadConfigAndGenerate(configFiles, fipsAvailable, haCertLabel, haGroupCertLabel, log)
|
||||
}
|
||||
|
||||
func loadConfigAndGenerate(templateConfigs map[string]string, fipsAvailable bool, haCertLabel, haGroupCertLabel string, log *logger.Logger) error {
|
||||
cfg, err := loadConfigFromEnv(log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = cfg.updateTLS(fipsAvailable, haCertLabel, haGroupCertLabel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for outputPath, templateFile := range templateConfigs {
|
||||
err := cfg.generate(templateFile, outputPath, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func envConfigPresent() bool {
|
||||
checkVars := []string{
|
||||
"MQ_NATIVE_HA_INSTANCE_0_NAME",
|
||||
"MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS",
|
||||
"MQ_NATIVE_HA_INSTANCE_1_NAME",
|
||||
"MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS",
|
||||
"MQ_NATIVE_HA_INSTANCE_2_NAME",
|
||||
"MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS",
|
||||
"MQ_NATIVE_HA_TLS",
|
||||
"MQ_NATIVE_HA_CIPHERSPEC",
|
||||
}
|
||||
for _, checkVar := range checkVars {
|
||||
if os.Getenv(checkVar) != "" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func loadConfigFromEnv(log *logger.Logger) (*haConfig, error) {
|
||||
cfg := &haConfig{
|
||||
Name: os.Getenv("HOSTNAME"),
|
||||
Instances: [3]haInstance{
|
||||
{
|
||||
Name: os.Getenv("MQ_NATIVE_HA_INSTANCE_0_NAME"),
|
||||
ReplicationAddress: os.Getenv("MQ_NATIVE_HA_INSTANCE_0_REPLICATION_ADDRESS"),
|
||||
},
|
||||
{
|
||||
Name: os.Getenv("MQ_NATIVE_HA_INSTANCE_1_NAME"),
|
||||
ReplicationAddress: os.Getenv("MQ_NATIVE_HA_INSTANCE_1_REPLICATION_ADDRESS"),
|
||||
},
|
||||
{
|
||||
Name: os.Getenv("MQ_NATIVE_HA_INSTANCE_2_NAME"),
|
||||
ReplicationAddress: os.Getenv("MQ_NATIVE_HA_INSTANCE_2_REPLICATION_ADDRESS"),
|
||||
},
|
||||
},
|
||||
Group: haGroupConfig{
|
||||
Local: haLocalGroupConfig{
|
||||
Address: os.Getenv("MQ_NATIVE_HA_GROUP_LOCAL_ADDRESS"),
|
||||
Name: os.Getenv("MQ_NATIVE_HA_GROUP_LOCAL_NAME"),
|
||||
Role: os.Getenv("MQ_NATIVE_HA_GROUP_ROLE"),
|
||||
},
|
||||
Recovery: haRecoveryGroupConfig{
|
||||
Address: os.Getenv("MQ_NATIVE_HA_GROUP_REPLICATION_ADDRESS"),
|
||||
Name: os.Getenv("MQ_NATIVE_HA_GROUP_RECOVERY_NAME"),
|
||||
Enabled: os.Getenv("MQ_NATIVE_HA_GROUP_RECOVERY_ENABLED") != "false",
|
||||
},
|
||||
CipherSpec: os.Getenv("MQ_NATIVE_HA_GROUP_CIPHERSPEC"),
|
||||
},
|
||||
CipherSpec: os.Getenv("MQ_NATIVE_HA_CIPHERSPEC"),
|
||||
keyRepository: os.Getenv("MQ_NATIVE_HA_KEY_REPOSITORY"),
|
||||
}
|
||||
|
||||
if cfg.Group.Recovery.Name == "" {
|
||||
cfg.Group.Recovery.Enabled = false
|
||||
}
|
||||
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
type haConfig struct {
|
||||
Name string
|
||||
Instances [3]haInstance
|
||||
Group haGroupConfig
|
||||
|
||||
haTLSEnabled bool
|
||||
CipherSpec string
|
||||
CertificateLabel string
|
||||
keyRepository string
|
||||
fipsAvailable bool
|
||||
}
|
||||
|
||||
func (h haConfig) ShouldConfigureTLS() bool {
|
||||
if h.haTLSEnabled {
|
||||
return true
|
||||
}
|
||||
if h.Group.Local.Name != "" {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h haConfig) SSLFipsRequired() string {
|
||||
return yesNo(h.fipsAvailable).String()
|
||||
}
|
||||
|
||||
func (h *haConfig) updateTLS(fipsAvailable bool, haCertLabel, haGroupCertLabel string) error {
|
||||
if haCertLabel != "" {
|
||||
h.CertificateLabel = haCertLabel
|
||||
h.haTLSEnabled = true
|
||||
}
|
||||
if haGroupCertLabel != "" {
|
||||
h.Group.CertificateLabel = haGroupCertLabel
|
||||
h.haTLSEnabled = true
|
||||
}
|
||||
|
||||
h.fipsAvailable = fipsAvailable
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h haConfig) generate(templatePath string, outputPath string, log *logger.Logger) error {
|
||||
return mqtemplate.ProcessTemplateFile(templatePath, outputPath, h, log)
|
||||
}
|
||||
|
||||
func (h haConfig) KeyRepository() string {
|
||||
if h.keyRepository != "" {
|
||||
return h.keyRepository
|
||||
}
|
||||
return "/run/runmqserver/ha/tls/key"
|
||||
}
|
||||
|
||||
type haGroupConfig struct {
|
||||
Local haLocalGroupConfig
|
||||
Recovery haRecoveryGroupConfig
|
||||
CipherSpec string
|
||||
CertificateLabel string
|
||||
}
|
||||
|
||||
type haLocalGroupConfig struct {
|
||||
Name string
|
||||
Role string
|
||||
Address string
|
||||
}
|
||||
type haRecoveryGroupConfig struct {
|
||||
Name string
|
||||
Enabled yesNo
|
||||
Address string
|
||||
}
|
||||
|
||||
type haInstance struct {
|
||||
Name string
|
||||
ReplicationAddress string
|
||||
}
|
||||
|
||||
type yesNo bool
|
||||
|
||||
func (yn yesNo) String() string {
|
||||
if yn {
|
||||
return "Yes"
|
||||
}
|
||||
return "No"
|
||||
}
|
||||
545
internal/ha/ha_test.go
Normal file
545
internal/ha/ha_test.go
Normal file
@@ -0,0 +1,545 @@
|
||||
/*
|
||||
© 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
|
||||
}
|
||||
16
internal/ha/test_fixtures/envcfg/group-live-minimal.ini
Normal file
16
internal/ha/test_fixtures/envcfg/group-live-minimal.ini
Normal file
@@ -0,0 +1,16 @@
|
||||
NativeHALocalInstance:
|
||||
GroupName=alpha
|
||||
GroupRole=Live
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance0
|
||||
ReplicationAddress=test-config-instance0(9145)
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance1
|
||||
ReplicationAddress=test-config-instance1(9145)
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance2
|
||||
ReplicationAddress=test-config-instance2(9145)
|
||||
NativeHARecoveryGroup:
|
||||
GroupName=beta
|
||||
Enabled=Yes
|
||||
ReplicationAddress=beta-address(4445)
|
||||
19
internal/ha/test_fixtures/envcfg/group-live-plain-ha.ini
Normal file
19
internal/ha/test_fixtures/envcfg/group-live-plain-ha.ini
Normal file
@@ -0,0 +1,19 @@
|
||||
NativeHALocalInstance:
|
||||
CipherSpec=NULL
|
||||
GroupName=alpha
|
||||
GroupCipherSpec=ANY_TLS
|
||||
GroupRole=Live
|
||||
GroupLocalAddress=(4445)
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance0
|
||||
ReplicationAddress=test-config-instance0(9145)
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance1
|
||||
ReplicationAddress=test-config-instance1(9145)
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance2
|
||||
ReplicationAddress=test-config-instance2(9145)
|
||||
NativeHARecoveryGroup:
|
||||
GroupName=beta
|
||||
Enabled=Yes
|
||||
ReplicationAddress=beta-address(4445)
|
||||
16
internal/ha/test_fixtures/envcfg/group-recovery-minimal.ini
Normal file
16
internal/ha/test_fixtures/envcfg/group-recovery-minimal.ini
Normal file
@@ -0,0 +1,16 @@
|
||||
NativeHALocalInstance:
|
||||
GroupName=beta
|
||||
GroupRole=Recovery
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance0
|
||||
ReplicationAddress=test-config-instance0(9145)
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance1
|
||||
ReplicationAddress=test-config-instance1(9145)
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance2
|
||||
ReplicationAddress=test-config-instance2(9145)
|
||||
NativeHARecoveryGroup:
|
||||
GroupName=alpha
|
||||
Enabled=Yes
|
||||
ReplicationAddress=alpha-address(4445)
|
||||
10
internal/ha/test_fixtures/envcfg/minimal-config.ini
Normal file
10
internal/ha/test_fixtures/envcfg/minimal-config.ini
Normal file
@@ -0,0 +1,10 @@
|
||||
NativeHALocalInstance:
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance0
|
||||
ReplicationAddress=test-config-instance0(9145)
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance1
|
||||
ReplicationAddress=test-config-instance1(9145)
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance2
|
||||
ReplicationAddress=test-config-instance2(9145)
|
||||
11
internal/ha/test_fixtures/envcfg/tls-full.ini
Normal file
11
internal/ha/test_fixtures/envcfg/tls-full.ini
Normal file
@@ -0,0 +1,11 @@
|
||||
NativeHALocalInstance:
|
||||
CipherSpec=some-cipher
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance0
|
||||
ReplicationAddress=test-config-instance0(9145)
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance1
|
||||
ReplicationAddress=test-config-instance1(9145)
|
||||
NativeHAInstance:
|
||||
Name=test-config-instance2
|
||||
ReplicationAddress=test-config-instance2(9145)
|
||||
3
internal/ha/test_fixtures/instance/fips.ini
Normal file
3
internal/ha/test_fixtures/instance/fips.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
NativeHALocalInstance:
|
||||
Name=test-config
|
||||
SSLFipsRequired=Yes
|
||||
3
internal/ha/test_fixtures/instance/no-fips.ini
Normal file
3
internal/ha/test_fixtures/instance/no-fips.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
NativeHALocalInstance:
|
||||
Name=test-config
|
||||
SSLFipsRequired=No
|
||||
3
internal/ha/test_fixtures/keystore/group-only.ini
Normal file
3
internal/ha/test_fixtures/keystore/group-only.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
NativeHALocalInstance:
|
||||
GroupCertificateLabel=recoveryTLS
|
||||
KeyRepository=/run/runmqserver/ha/tls/key
|
||||
4
internal/ha/test_fixtures/keystore/ha-group.ini
Normal file
4
internal/ha/test_fixtures/keystore/ha-group.ini
Normal file
@@ -0,0 +1,4 @@
|
||||
NativeHALocalInstance:
|
||||
CertificateLabel=baseTLS
|
||||
GroupCertificateLabel=recoveryTLS
|
||||
KeyRepository=/run/runmqserver/ha/tls/key
|
||||
3
internal/ha/test_fixtures/keystore/ha-only.ini
Normal file
3
internal/ha/test_fixtures/keystore/ha-only.ini
Normal file
@@ -0,0 +1,3 @@
|
||||
NativeHALocalInstance:
|
||||
CertificateLabel=baseTLS
|
||||
KeyRepository=/run/runmqserver/ha/tls/key
|
||||
2
internal/ha/test_fixtures/keystore/overridden-path.ini
Normal file
2
internal/ha/test_fixtures/keystore/overridden-path.ini
Normal file
@@ -0,0 +1,2 @@
|
||||
NativeHALocalInstance:
|
||||
KeyRepository=/an/overridden/keystore
|
||||
257
internal/keystore/keystore.go
Normal file
257
internal/keystore/keystore.go
Normal file
@@ -0,0 +1,257 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2018, 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 keystore contains code to create and update keystores
|
||||
package keystore
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/internal/command"
|
||||
"github.com/ibm-messaging/mq-container/internal/fips"
|
||||
)
|
||||
|
||||
// KeyStore describes information about a keystore file
|
||||
type KeyStore struct {
|
||||
Filename string
|
||||
Password string
|
||||
keyStoreType string
|
||||
command string
|
||||
fipsEnabled bool
|
||||
}
|
||||
|
||||
// NewJKSKeyStore creates a new Java Key Store, managed by the runmqckm command
|
||||
func NewJKSKeyStore(filename, password string) *KeyStore {
|
||||
keyStore := &KeyStore{
|
||||
Filename: filename,
|
||||
Password: password,
|
||||
keyStoreType: "jks",
|
||||
command: "/opt/mqm/bin/runmqckm",
|
||||
fipsEnabled: fips.IsFIPSEnabled(),
|
||||
}
|
||||
|
||||
return keyStore
|
||||
}
|
||||
|
||||
// NewCMSKeyStore creates a new MQ CMS Key Store, managed by the runmqakm command
|
||||
func NewCMSKeyStore(filename, password string) *KeyStore {
|
||||
keyStore := &KeyStore{
|
||||
Filename: filename,
|
||||
Password: password,
|
||||
keyStoreType: "cms",
|
||||
command: "/opt/mqm/bin/runmqakm",
|
||||
fipsEnabled: fips.IsFIPSEnabled(),
|
||||
}
|
||||
|
||||
return keyStore
|
||||
}
|
||||
|
||||
// NewPKCS12KeyStore creates a new PKCS12 Key Store, managed by the runmqakm command
|
||||
func NewPKCS12KeyStore(filename, password string) *KeyStore {
|
||||
keyStore := &KeyStore{
|
||||
Filename: filename,
|
||||
Password: password,
|
||||
keyStoreType: "p12",
|
||||
command: "/opt/mqm/bin/runmqakm",
|
||||
fipsEnabled: fips.IsFIPSEnabled(),
|
||||
}
|
||||
|
||||
return keyStore
|
||||
}
|
||||
|
||||
// Create a key store, if it doesn't already exist
|
||||
func (ks *KeyStore) Create() error {
|
||||
_, err := os.Stat(ks.Filename)
|
||||
if err == nil {
|
||||
// Keystore already exists so we should refresh it by deleting it.
|
||||
extension := filepath.Ext(ks.Filename)
|
||||
if ks.keyStoreType == "cms" {
|
||||
// Only delete these when we are refreshing the kdb keystore
|
||||
stashFile := ks.Filename[0:len(ks.Filename)-len(extension)] + ".sth"
|
||||
rdbFile := ks.Filename[0:len(ks.Filename)-len(extension)] + ".rdb"
|
||||
crlFile := ks.Filename[0:len(ks.Filename)-len(extension)] + ".crl"
|
||||
err = os.Remove(stashFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Remove(rdbFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = os.Remove(crlFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = os.Remove(ks.Filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if !os.IsNotExist(err) {
|
||||
// If the keystore exists but cannot be accessed then return the error
|
||||
return err
|
||||
}
|
||||
|
||||
// Create the keystore now we're sure it doesn't exist
|
||||
out, _, err := command.Run(ks.command, "-keydb", "-create", ks.getFipsEnabledFlag(), "-type", ks.keyStoreType, "-db", ks.Filename, "-pw", ks.Password, "-stash")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running \"%v -keydb -create\": %v %s", ks.command, err, out)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateStash creates a key stash, if it doesn't already exist
|
||||
func (ks *KeyStore) CreateStash() error {
|
||||
extension := filepath.Ext(ks.Filename)
|
||||
stashFile := ks.Filename[0:len(ks.Filename)-len(extension)] + ".sth"
|
||||
_, err := os.Stat(stashFile)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
out, _, err := command.Run(ks.command, "-keydb", ks.getFipsEnabledFlag(), "-stashpw", "-type", ks.keyStoreType, "-db", ks.Filename, "-pw", ks.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running \"%v -keydb -stashpw\": %v %s", ks.command, err, out)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Import imports a certificate file in the keystore
|
||||
func (ks *KeyStore) Import(inputFile, password string) error {
|
||||
out, _, err := command.Run(ks.command, "-cert", "-import", ks.getFipsEnabledFlag(), "-file", inputFile, "-pw", password, "-target", ks.Filename, "-target_pw", ks.Password, "-target_type", ks.keyStoreType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running \"%v -cert -import\": %v %s", ks.command, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CreateSelfSignedCertificate creates a self-signed certificate in the keystore
|
||||
func (ks *KeyStore) CreateSelfSignedCertificate(label, dn, hostname string) error {
|
||||
out, _, err := command.Run(ks.command, "-cert", "-create", ks.getFipsEnabledFlag(), "-db", ks.Filename, "-pw", ks.Password, "-label", label, "-dn", dn, "-san_dnsname", hostname, "-size 2048 -sig_alg sha512 -eku serverAuth")
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running \"%v -cert -create\": %v %s", ks.command, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add adds a CA certificate to the keystore
|
||||
func (ks *KeyStore) Add(inputFile, label string) error {
|
||||
out, _, err := command.Run(ks.command, "-cert", "-add", ks.getFipsEnabledFlag(), "-db", ks.Filename, "-type", ks.keyStoreType, "-pw", ks.Password, "-file", inputFile, "-label", label)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running \"%v -cert -add\": %v %s", ks.command, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Add adds a CA certificate to the keystore
|
||||
func (ks *KeyStore) AddNoLabel(inputFile string) error {
|
||||
out, _, err := command.Run(ks.command, "-cert", "-add", ks.getFipsEnabledFlag(), "-db", ks.Filename, "-type", ks.keyStoreType, "-pw", ks.Password, "-file", inputFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running \"%v -cert -add\": %v %s", ks.command, err, out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCertificateLabels returns the labels of all certificates in the key store
|
||||
func (ks *KeyStore) GetCertificateLabels() ([]string, error) {
|
||||
out, _, err := command.Run(ks.command, "-cert", "-list", ks.getFipsEnabledFlag(), "-type", ks.keyStoreType, "-db", ks.Filename, "-pw", ks.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error running \"%v -cert -list\": %v %s", ks.command, err, out)
|
||||
}
|
||||
scanner := bufio.NewScanner(strings.NewReader(out))
|
||||
var labels []string
|
||||
for scanner.Scan() {
|
||||
s := scanner.Text()
|
||||
if strings.HasPrefix(s, "-") || strings.HasPrefix(s, "*-") {
|
||||
s := strings.TrimLeft(s, "-*")
|
||||
labels = append(labels, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
err = scanner.Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
// RenameCertificate renames the specified certificate
|
||||
func (ks *KeyStore) RenameCertificate(from, to string) error {
|
||||
if ks.command == "/opt/mqm/bin/runmqakm" {
|
||||
// runmqakm can't handle certs with ' in them so just use capicmd
|
||||
// Overriding gosec here as this function is in an internal package and only callable by our internal functions.
|
||||
// #nosec G204
|
||||
cmd := exec.Command("/opt/mqm/gskit8/bin/gsk8capicmd_64", "-cert", "-rename", "-db", ks.Filename, "-pw", ks.Password, "-label", from, "-new_label", to)
|
||||
cmd.Env = append(os.Environ(), "LD_LIBRARY_PATH=/opt/mqm/gskit8/lib64/:/opt/mqm/gskit8/lib")
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running \"%v -cert -rename\": %v %s", "/opt/mqm/gskit8/bin/gsk8capicmd_64", err, out)
|
||||
}
|
||||
} else {
|
||||
out, _, err := command.Run(ks.command, "-cert", "-rename", "-db", ks.Filename, "-pw", ks.Password, "-label", from, "-new_label", to)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running \"%v -cert -rename\": %v %s", ks.command, err, out)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ListAllCertificates Lists all certificates in the keystore
|
||||
func (ks *KeyStore) ListAllCertificates() ([]string, error) {
|
||||
out, _, err := command.Run(ks.command, "-cert", "-list", ks.getFipsEnabledFlag(), "-type", ks.keyStoreType, "-db", ks.Filename, "-pw", ks.Password)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error running \"%v -cert -list\": %v %s", ks.command, err, out)
|
||||
}
|
||||
scanner := bufio.NewScanner(strings.NewReader(out))
|
||||
var labels []string
|
||||
for scanner.Scan() {
|
||||
s := scanner.Text()
|
||||
// Check for trusted certficates as well here as this method can
|
||||
// be called for trusted store as well.
|
||||
if strings.HasPrefix(s, "-") || strings.HasPrefix(s, "*-") || strings.HasPrefix(s, "!") {
|
||||
s := strings.TrimLeft(s, "-*!")
|
||||
labels = append(labels, strings.TrimSpace(s))
|
||||
}
|
||||
}
|
||||
err = scanner.Err()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return labels, nil
|
||||
}
|
||||
|
||||
// Returns the FIPS flag. True if enabled else false
|
||||
func (ks *KeyStore) IsFIPSEnabled() bool {
|
||||
return ks.fipsEnabled
|
||||
}
|
||||
|
||||
// getFipsEnabledFlag returns the appropriate flag for runmqakm/runmqckm commands
|
||||
// to enable or disable FIPS.
|
||||
func (ks *KeyStore) getFipsEnabledFlag() string {
|
||||
if ks.fipsEnabled {
|
||||
return "-fips"
|
||||
} else {
|
||||
// In the GSKit command line, FIPS mode is enabled by default, so explicitly disable it
|
||||
return "-fips false"
|
||||
}
|
||||
}
|
||||
191
internal/metrics/exporter.go
Normal file
191
internal/metrics/exporter.go
Normal file
@@ -0,0 +1,191 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2018, 2019
|
||||
|
||||
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 metrics contains code to provide metrics for the queue manager
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"github.com/ibm-messaging/mq-container/pkg/logger"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
)
|
||||
|
||||
const (
|
||||
namespace = "ibmmq"
|
||||
qmgrPrefix = "qmgr"
|
||||
qmgrLabel = "qmgr"
|
||||
objectPrefix = "object"
|
||||
objectLabel = "object"
|
||||
)
|
||||
|
||||
type exporter struct {
|
||||
qmName string
|
||||
gaugeMap map[string]*prometheus.GaugeVec
|
||||
counterMap map[string]*prometheus.CounterVec
|
||||
firstCollect bool
|
||||
log *logger.Logger
|
||||
}
|
||||
|
||||
func newExporter(qmName string, log *logger.Logger) *exporter {
|
||||
return &exporter{
|
||||
qmName: qmName,
|
||||
gaugeMap: make(map[string]*prometheus.GaugeVec),
|
||||
counterMap: make(map[string]*prometheus.CounterVec),
|
||||
firstCollect: true,
|
||||
log: log,
|
||||
}
|
||||
}
|
||||
|
||||
// Describe provides details of all available metrics
|
||||
func (e *exporter) Describe(ch chan<- *prometheus.Desc) {
|
||||
|
||||
requestChannel <- false
|
||||
response := <-responseChannel
|
||||
|
||||
for key, metric := range response {
|
||||
|
||||
if metric.isDelta {
|
||||
// For delta type metrics - allocate a Prometheus Counter
|
||||
counterVec := createCounterVec(metric.name, metric.description, metric.objectType)
|
||||
e.counterMap[key] = counterVec
|
||||
|
||||
// Describe metric
|
||||
counterVec.Describe(ch)
|
||||
|
||||
} else {
|
||||
// For non-delta type metrics - allocate a Prometheus Gauge
|
||||
gaugeVec := createGaugeVec(metric.name, metric.description, metric.objectType)
|
||||
e.gaugeMap[key] = gaugeVec
|
||||
|
||||
// Describe metric
|
||||
gaugeVec.Describe(ch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect is called at regular intervals to provide the current metric data
|
||||
func (e *exporter) Collect(ch chan<- prometheus.Metric) {
|
||||
|
||||
requestChannel <- true
|
||||
response := <-responseChannel
|
||||
|
||||
for key, metric := range response {
|
||||
|
||||
if metric.isDelta {
|
||||
// For delta type metrics - update their Prometheus Counter
|
||||
counterVec := e.counterMap[key]
|
||||
|
||||
// Populate Prometheus Counter with metric values
|
||||
// - Skip on first collect to avoid build-up of accumulated values
|
||||
if !e.firstCollect {
|
||||
for label, value := range metric.values {
|
||||
var err error
|
||||
var counter prometheus.Counter
|
||||
|
||||
if label == qmgrLabelValue {
|
||||
counter, err = counterVec.GetMetricWithLabelValues(e.qmName)
|
||||
} else {
|
||||
counter, err = counterVec.GetMetricWithLabelValues(label, e.qmName)
|
||||
}
|
||||
if err == nil {
|
||||
counter.Add(value)
|
||||
} else {
|
||||
e.log.Errorf("Metrics Error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect metric
|
||||
counterVec.Collect(ch)
|
||||
|
||||
} else {
|
||||
// For non-delta type metrics - reset their Prometheus Gauge
|
||||
gaugeVec := e.gaugeMap[key]
|
||||
gaugeVec.Reset()
|
||||
|
||||
// Populate Prometheus Gauge with metric values
|
||||
// - Skip on first collect to avoid build-up of accumulated values
|
||||
if !e.firstCollect {
|
||||
for label, value := range metric.values {
|
||||
var err error
|
||||
var gauge prometheus.Gauge
|
||||
|
||||
if label == qmgrLabelValue {
|
||||
gauge, err = gaugeVec.GetMetricWithLabelValues(e.qmName)
|
||||
} else {
|
||||
gauge, err = gaugeVec.GetMetricWithLabelValues(label, e.qmName)
|
||||
}
|
||||
if err == nil {
|
||||
gauge.Set(value)
|
||||
} else {
|
||||
e.log.Errorf("Metrics Error: %s", err.Error())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect metric
|
||||
gaugeVec.Collect(ch)
|
||||
}
|
||||
}
|
||||
|
||||
if e.firstCollect {
|
||||
e.firstCollect = false
|
||||
}
|
||||
}
|
||||
|
||||
// createCounterVec returns a Prometheus CounterVec populated with metric details
|
||||
func createCounterVec(name, description string, objectType bool) *prometheus.CounterVec {
|
||||
|
||||
prefix, labels := getVecDetails(objectType)
|
||||
|
||||
counterVec := prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Namespace: namespace,
|
||||
Name: prefix + "_" + name,
|
||||
Help: description,
|
||||
},
|
||||
labels,
|
||||
)
|
||||
return counterVec
|
||||
}
|
||||
|
||||
// createGaugeVec returns a Prometheus GaugeVec populated with metric details
|
||||
func createGaugeVec(name, description string, objectType bool) *prometheus.GaugeVec {
|
||||
|
||||
prefix, labels := getVecDetails(objectType)
|
||||
|
||||
gaugeVec := prometheus.NewGaugeVec(
|
||||
prometheus.GaugeOpts{
|
||||
Namespace: namespace,
|
||||
Name: prefix + "_" + name,
|
||||
Help: description,
|
||||
},
|
||||
labels,
|
||||
)
|
||||
return gaugeVec
|
||||
}
|
||||
|
||||
// getVecDetails returns the required prefix and labels for a metric
|
||||
func getVecDetails(objectType bool) (prefix string, labels []string) {
|
||||
|
||||
prefix = qmgrPrefix
|
||||
labels = []string{qmgrLabel}
|
||||
|
||||
if objectType {
|
||||
prefix = objectPrefix
|
||||
labels = []string{objectLabel, qmgrLabel}
|
||||
}
|
||||
return prefix, labels
|
||||
}
|
||||
204
internal/metrics/exporter_test.go
Normal file
204
internal/metrics/exporter_test.go
Normal file
@@ -0,0 +1,204 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2018
|
||||
|
||||
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 metrics
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ibm-messaging/mq-golang/ibmmq"
|
||||
"github.com/ibm-messaging/mq-golang/mqmetric"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
dto "github.com/prometheus/client_model/go"
|
||||
)
|
||||
|
||||
func TestDescribe_Counter(t *testing.T) {
|
||||
testDescribe(t, true)
|
||||
}
|
||||
|
||||
func TestDescribe_Gauge(t *testing.T) {
|
||||
testDescribe(t, false)
|
||||
}
|
||||
|
||||
func testDescribe(t *testing.T, isDelta bool) {
|
||||
|
||||
teardownTestCase := setupTestCase(false)
|
||||
defer teardownTestCase()
|
||||
log := getTestLogger()
|
||||
|
||||
ch := make(chan *prometheus.Desc)
|
||||
go func() {
|
||||
exporter := newExporter("qmName", log)
|
||||
exporter.Describe(ch)
|
||||
}()
|
||||
|
||||
collect := <-requestChannel
|
||||
if collect {
|
||||
t.Errorf("Received unexpected collect request")
|
||||
}
|
||||
|
||||
if isDelta {
|
||||
mqmetric.Metrics.Classes[0].Types[0].Elements[0].Datatype = ibmmq.MQIAMO_MONITOR_DELTA
|
||||
}
|
||||
metrics, _ := initialiseMetrics(log)
|
||||
responseChannel <- metrics
|
||||
|
||||
select {
|
||||
case prometheusDesc := <-ch:
|
||||
expected := "Desc{fqName: \"ibmmq_qmgr_" + testElement1Name + "\", help: \"" + testElement1Description + "\", constLabels: {}, variableLabels: [qmgr]}"
|
||||
actual := prometheusDesc.String()
|
||||
if actual != expected {
|
||||
t.Errorf("Expected value=%s; actual %s", expected, actual)
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Error("Did not receive channel response from describe")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCollect_Counter(t *testing.T) {
|
||||
testCollect(t, true)
|
||||
}
|
||||
|
||||
func TestCollect_Gauge(t *testing.T) {
|
||||
testCollect(t, false)
|
||||
}
|
||||
|
||||
func testCollect(t *testing.T, isDelta bool) {
|
||||
|
||||
teardownTestCase := setupTestCase(false)
|
||||
defer teardownTestCase()
|
||||
log := getTestLogger()
|
||||
|
||||
exporter := newExporter("qmName", log)
|
||||
if isDelta {
|
||||
exporter.counterMap[testKey1] = createCounterVec(testElement1Name, testElement1Description, false)
|
||||
} else {
|
||||
exporter.gaugeMap[testKey1] = createGaugeVec(testElement1Name, testElement1Description, false)
|
||||
}
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
|
||||
ch := make(chan prometheus.Metric)
|
||||
go func() {
|
||||
exporter.Collect(ch)
|
||||
close(ch)
|
||||
}()
|
||||
|
||||
collect := <-requestChannel
|
||||
if !collect {
|
||||
t.Errorf("Received unexpected describe request")
|
||||
}
|
||||
|
||||
populateTestMetrics(i, false)
|
||||
if isDelta {
|
||||
mqmetric.Metrics.Classes[0].Types[0].Elements[0].Datatype = ibmmq.MQIAMO_MONITOR_DELTA
|
||||
}
|
||||
metrics, _ := initialiseMetrics(log)
|
||||
updateMetrics(metrics)
|
||||
responseChannel <- metrics
|
||||
|
||||
select {
|
||||
case <-ch:
|
||||
var actual float64
|
||||
prometheusMetric := dto.Metric{}
|
||||
if isDelta {
|
||||
exporter.counterMap[testKey1].WithLabelValues("qmName").Write(&prometheusMetric)
|
||||
actual = prometheusMetric.GetCounter().GetValue()
|
||||
} else {
|
||||
exporter.gaugeMap[testKey1].WithLabelValues("qmName").Write(&prometheusMetric)
|
||||
actual = prometheusMetric.GetGauge().GetValue()
|
||||
}
|
||||
|
||||
if i == 1 {
|
||||
if actual != float64(0) {
|
||||
t.Errorf("Expected values to be zero on first collect; actual %f", actual)
|
||||
}
|
||||
} else if isDelta && i != 2 {
|
||||
if actual != float64(i+(i-1)) {
|
||||
t.Errorf("Expected value=%f; actual %f", float64(i+(i-1)), actual)
|
||||
}
|
||||
} else if actual != float64(i) {
|
||||
t.Errorf("Expected value=%f; actual %f", float64(i), actual)
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
t.Error("Did not receive channel response from collect")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCounterVec(t *testing.T) {
|
||||
|
||||
ch := make(chan *prometheus.Desc)
|
||||
counterVec := createCounterVec("MetricName", "MetricDescription", false)
|
||||
go func() {
|
||||
counterVec.Describe(ch)
|
||||
}()
|
||||
description := <-ch
|
||||
|
||||
expected := "Desc{fqName: \"ibmmq_qmgr_MetricName\", help: \"MetricDescription\", constLabels: {}, variableLabels: [qmgr]}"
|
||||
actual := description.String()
|
||||
if actual != expected {
|
||||
t.Errorf("Expected value=%s; actual %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateCounterVec_ObjectLabel(t *testing.T) {
|
||||
|
||||
ch := make(chan *prometheus.Desc)
|
||||
counterVec := createCounterVec("MetricName", "MetricDescription", true)
|
||||
go func() {
|
||||
counterVec.Describe(ch)
|
||||
}()
|
||||
description := <-ch
|
||||
|
||||
expected := "Desc{fqName: \"ibmmq_object_MetricName\", help: \"MetricDescription\", constLabels: {}, variableLabels: [object qmgr]}"
|
||||
actual := description.String()
|
||||
if actual != expected {
|
||||
t.Errorf("Expected value=%s; actual %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateGaugeVec(t *testing.T) {
|
||||
|
||||
ch := make(chan *prometheus.Desc)
|
||||
gaugeVec := createGaugeVec("MetricName", "MetricDescription", false)
|
||||
go func() {
|
||||
gaugeVec.Describe(ch)
|
||||
}()
|
||||
description := <-ch
|
||||
|
||||
expected := "Desc{fqName: \"ibmmq_qmgr_MetricName\", help: \"MetricDescription\", constLabels: {}, variableLabels: [qmgr]}"
|
||||
actual := description.String()
|
||||
if actual != expected {
|
||||
t.Errorf("Expected value=%s; actual %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateGaugeVec_ObjectLabel(t *testing.T) {
|
||||
|
||||
ch := make(chan *prometheus.Desc)
|
||||
gaugeVec := createGaugeVec("MetricName", "MetricDescription", true)
|
||||
go func() {
|
||||
gaugeVec.Describe(ch)
|
||||
}()
|
||||
description := <-ch
|
||||
|
||||
expected := "Desc{fqName: \"ibmmq_object_MetricName\", help: \"MetricDescription\", constLabels: {}, variableLabels: [object qmgr]}"
|
||||
actual := description.String()
|
||||
if actual != expected {
|
||||
t.Errorf("Expected value=%s; actual %s", expected, actual)
|
||||
}
|
||||
}
|
||||
124
internal/metrics/mapping.go
Normal file
124
internal/metrics/mapping.go
Normal file
@@ -0,0 +1,124 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2018
|
||||
|
||||
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 metrics contains code to provide metrics for the queue manager
|
||||
package metrics
|
||||
|
||||
type metricLookup struct {
|
||||
name string
|
||||
enabled bool
|
||||
}
|
||||
|
||||
// generateMetricNamesMap generates metric names mapped from their description
|
||||
func generateMetricNamesMap() map[string]metricLookup {
|
||||
|
||||
metricNamesMap := map[string]metricLookup{
|
||||
"CPU/SystemSummary/CPU load - one minute average": metricLookup{"cpu_load_one_minute_average_percentage", true},
|
||||
"CPU/SystemSummary/CPU load - five minute average": metricLookup{"cpu_load_five_minute_average_percentage", true},
|
||||
"CPU/SystemSummary/CPU load - fifteen minute average": metricLookup{"cpu_load_fifteen_minute_average_percentage", true},
|
||||
"CPU/SystemSummary/System CPU time percentage": metricLookup{"system_cpu_time_percentage", true},
|
||||
"CPU/SystemSummary/User CPU time percentage": metricLookup{"user_cpu_time_percentage", true},
|
||||
"CPU/SystemSummary/RAM free percentage": metricLookup{"ram_free_percentage", true},
|
||||
"CPU/SystemSummary/RAM total bytes": metricLookup{"system_ram_size_bytes", false},
|
||||
"CPU/QMgrSummary/System CPU time - percentage estimate for queue manager": metricLookup{"system_cpu_time_estimate_for_queue_manager_percentage", true},
|
||||
"CPU/QMgrSummary/User CPU time - percentage estimate for queue manager": metricLookup{"user_cpu_time_estimate_for_queue_manager_percentage", true},
|
||||
"CPU/QMgrSummary/RAM total bytes - estimate for queue manager": metricLookup{"ram_usage_estimate_for_queue_manager_bytes", true},
|
||||
"DISK/SystemSummary/MQ trace file system - free space": metricLookup{"trace_file_system_free_space_percentage", true},
|
||||
"DISK/SystemSummary/MQ trace file system - bytes in use": metricLookup{"trace_file_system_in_use_bytes", true},
|
||||
"DISK/SystemSummary/MQ errors file system - free space": metricLookup{"errors_file_system_free_space_percentage", true},
|
||||
"DISK/SystemSummary/MQ errors file system - bytes in use": metricLookup{"errors_file_system_in_use_bytes", true},
|
||||
"DISK/SystemSummary/MQ FDC file count": metricLookup{"fdc_files", true},
|
||||
"DISK/QMgrSummary/Queue Manager file system - free space": metricLookup{"queue_manager_file_system_free_space_percentage", true},
|
||||
"DISK/QMgrSummary/Queue Manager file system - bytes in use": metricLookup{"queue_manager_file_system_in_use_bytes", true},
|
||||
"DISK/Log/Log - logical bytes written": metricLookup{"log_logical_written_bytes_total", true},
|
||||
"DISK/Log/Log - physical bytes written": metricLookup{"log_physical_written_bytes_total", true},
|
||||
"DISK/Log/Log - current primary space in use": metricLookup{"log_primary_space_in_use_percentage", true},
|
||||
"DISK/Log/Log - workload primary space utilization": metricLookup{"log_workload_primary_space_utilization_percentage", true},
|
||||
"DISK/Log/Log - write latency": metricLookup{"log_write_latency_seconds", true},
|
||||
"DISK/Log/Log - bytes max": metricLookup{"log_max_bytes", true},
|
||||
"DISK/Log/Log - write size": metricLookup{"log_write_size_bytes", true},
|
||||
"DISK/Log/Log - bytes in use": metricLookup{"log_in_use_bytes", true},
|
||||
"DISK/Log/Log file system - bytes max": metricLookup{"log_file_system_max_bytes", true},
|
||||
"DISK/Log/Log file system - bytes in use": metricLookup{"log_file_system_in_use_bytes", true},
|
||||
"DISK/Log/Log - bytes occupied by reusable extents": metricLookup{"log_occupied_by_reusable_extents_bytes", true},
|
||||
"DISK/Log/Log - bytes occupied by extents waiting to be archived": metricLookup{"log_occupied_by_extents_waiting_to_be_archived_bytes", true},
|
||||
"DISK/Log/Log - bytes required for media recovery": metricLookup{"log_required_for_media_recovery_bytes", true},
|
||||
"STATMQI/SUBSCRIBE/Create durable subscription count": metricLookup{"durable_subscription_create_total", true},
|
||||
"STATMQI/SUBSCRIBE/Alter durable subscription count": metricLookup{"durable_subscription_alter_total", true},
|
||||
"STATMQI/SUBSCRIBE/Resume durable subscription count": metricLookup{"durable_subscription_resume_total", true},
|
||||
"STATMQI/SUBSCRIBE/Delete durable subscription count": metricLookup{"durable_subscription_delete_total", true},
|
||||
"STATMQI/SUBSCRIBE/Create non-durable subscription count": metricLookup{"non_durable_subscription_create_total", true},
|
||||
"STATMQI/SUBSCRIBE/Delete non-durable subscription count": metricLookup{"non_durable_subscription_delete_total", true},
|
||||
"STATMQI/SUBSCRIBE/Failed create/alter/resume subscription count": metricLookup{"failed_subscription_create_alter_resume_total", true},
|
||||
"STATMQI/SUBSCRIBE/Subscription delete failure count": metricLookup{"failed_subscription_delete_total", true},
|
||||
"STATMQI/SUBSCRIBE/MQSUBRQ count": metricLookup{"mqsubrq_total", true},
|
||||
"STATMQI/SUBSCRIBE/Failed MQSUBRQ count": metricLookup{"failed_mqsubrq_total", true},
|
||||
"STATMQI/SUBSCRIBE/Durable subscriber - high water mark": metricLookup{"durable_subscriber_high_water_mark", false},
|
||||
"STATMQI/SUBSCRIBE/Durable subscriber - low water mark": metricLookup{"durable_subscriber_low_water_mark", false},
|
||||
"STATMQI/SUBSCRIBE/Non-durable subscriber - high water mark": metricLookup{"non_durable_subscriber_high_water_mark", false},
|
||||
"STATMQI/SUBSCRIBE/Non-durable subscriber - low water mark": metricLookup{"non_durable_subscriber_low_water_mark", false},
|
||||
"STATMQI/PUBLISH/Topic MQPUT/MQPUT1 interval total": metricLookup{"topic_mqput_mqput1_total", true},
|
||||
"STATMQI/PUBLISH/Interval total topic bytes put": metricLookup{"topic_put_bytes_total", true},
|
||||
"STATMQI/PUBLISH/Failed topic MQPUT/MQPUT1 count": metricLookup{"failed_topic_mqput_mqput1_total", true},
|
||||
"STATMQI/PUBLISH/Persistent - topic MQPUT/MQPUT1 count": metricLookup{"persistent_topic_mqput_mqput1_total", true},
|
||||
"STATMQI/PUBLISH/Non-persistent - topic MQPUT/MQPUT1 count": metricLookup{"non_persistent_topic_mqput_mqput1_total", true},
|
||||
"STATMQI/PUBLISH/Published to subscribers - message count": metricLookup{"published_to_subscribers_message_total", true},
|
||||
"STATMQI/PUBLISH/Published to subscribers - byte count": metricLookup{"published_to_subscribers_bytes_total", true},
|
||||
"STATMQI/CONNDISC/MQCONN/MQCONNX count": metricLookup{"mqconn_mqconnx_total", true},
|
||||
"STATMQI/CONNDISC/Failed MQCONN/MQCONNX count": metricLookup{"failed_mqconn_mqconnx_total", true},
|
||||
"STATMQI/CONNDISC/MQDISC count": metricLookup{"mqdisc_total", true},
|
||||
"STATMQI/CONNDISC/Concurrent connections - high water mark": metricLookup{"concurrent_connections_high_water_mark", false},
|
||||
"STATMQI/OPENCLOSE/MQOPEN count": metricLookup{"mqopen_total", true},
|
||||
"STATMQI/OPENCLOSE/Failed MQOPEN count": metricLookup{"failed_mqopen_total", true},
|
||||
"STATMQI/OPENCLOSE/MQCLOSE count": metricLookup{"mqclose_total", true},
|
||||
"STATMQI/OPENCLOSE/Failed MQCLOSE count": metricLookup{"failed_mqclose_total", true},
|
||||
"STATMQI/INQSET/MQINQ count": metricLookup{"mqinq_total", true},
|
||||
"STATMQI/INQSET/Failed MQINQ count": metricLookup{"failed_mqinq_total", true},
|
||||
"STATMQI/INQSET/MQSET count": metricLookup{"mqset_total", true},
|
||||
"STATMQI/INQSET/Failed MQSET count": metricLookup{"failed_mqset_total", true},
|
||||
"STATMQI/PUT/Persistent message MQPUT count": metricLookup{"persistent_message_mqput_total", true},
|
||||
"STATMQI/PUT/Persistent message MQPUT1 count": metricLookup{"persistent_message_mqput1_total", true},
|
||||
"STATMQI/PUT/Put persistent messages - byte count": metricLookup{"persistent_message_put_bytes_total", true},
|
||||
"STATMQI/PUT/Non-persistent message MQPUT count": metricLookup{"non_persistent_message_mqput_total", true},
|
||||
"STATMQI/PUT/Non-persistent message MQPUT1 count": metricLookup{"non_persistent_message_mqput1_total", true},
|
||||
"STATMQI/PUT/Put non-persistent messages - byte count": metricLookup{"non_persistent_message_put_bytes_total", true},
|
||||
"STATMQI/PUT/Interval total MQPUT/MQPUT1 count": metricLookup{"mqput_mqput1_total", true},
|
||||
"STATMQI/PUT/Interval total MQPUT/MQPUT1 byte count": metricLookup{"mqput_mqput1_bytes_total", true},
|
||||
"STATMQI/PUT/Failed MQPUT count": metricLookup{"failed_mqput_total", true},
|
||||
"STATMQI/PUT/Failed MQPUT1 count": metricLookup{"failed_mqput1_total", true},
|
||||
"STATMQI/PUT/MQSTAT count": metricLookup{"mqstat_total", true},
|
||||
"STATMQI/GET/Persistent message destructive get - count": metricLookup{"persistent_message_destructive_get_total", true},
|
||||
"STATMQI/GET/Persistent message browse - count": metricLookup{"persistent_message_browse_total", true},
|
||||
"STATMQI/GET/Got persistent messages - byte count": metricLookup{"persistent_message_get_bytes_total", true},
|
||||
"STATMQI/GET/Persistent message browse - byte count": metricLookup{"persistent_message_browse_bytes_total", true},
|
||||
"STATMQI/GET/Non-persistent message destructive get - count": metricLookup{"non_persistent_message_destructive_get_total", true},
|
||||
"STATMQI/GET/Non-persistent message browse - count": metricLookup{"non_persistent_message_browse_total", true},
|
||||
"STATMQI/GET/Got non-persistent messages - byte count": metricLookup{"non_persistent_message_get_bytes_total", true},
|
||||
"STATMQI/GET/Non-persistent message browse - byte count": metricLookup{"non_persistent_message_browse_bytes_total", true},
|
||||
"STATMQI/GET/Interval total destructive get- count": metricLookup{"destructive_get_total", true},
|
||||
"STATMQI/GET/Interval total destructive get - byte count": metricLookup{"destructive_get_bytes_total", true},
|
||||
"STATMQI/GET/Failed MQGET - count": metricLookup{"failed_mqget_total", true},
|
||||
"STATMQI/GET/Failed browse count": metricLookup{"failed_browse_total", true},
|
||||
"STATMQI/GET/MQCTL count": metricLookup{"mqctl_total", true},
|
||||
"STATMQI/GET/Expired message count": metricLookup{"expired_message_total", true},
|
||||
"STATMQI/GET/Purged queue count": metricLookup{"purged_queue_total", true},
|
||||
"STATMQI/GET/MQCB count": metricLookup{"mqcb_total", true},
|
||||
"STATMQI/GET/Failed MQCB count": metricLookup{"failed_mqcb_total", true},
|
||||
"STATMQI/SYNCPOINT/Commit count": metricLookup{"commit_total", true},
|
||||
"STATMQI/SYNCPOINT/Rollback count": metricLookup{"rollback_total", true},
|
||||
}
|
||||
return metricNamesMap
|
||||
}
|
||||
37
internal/metrics/mapping_test.go
Normal file
37
internal/metrics/mapping_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2018
|
||||
|
||||
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 metrics
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGenerateMetricNamesMap(t *testing.T) {
|
||||
|
||||
metricNamesMap := generateMetricNamesMap()
|
||||
|
||||
if len(metricNamesMap) != 93 {
|
||||
t.Errorf("Expected mapping-size=%d; actual %d", 93, len(metricNamesMap))
|
||||
}
|
||||
|
||||
actual, ok := metricNamesMap[testKey1]
|
||||
|
||||
if !ok {
|
||||
t.Errorf("No metric name mapping found for %s", testKey1)
|
||||
} else {
|
||||
if actual.name != testElement1Name {
|
||||
t.Errorf("Expected metric name=%s; actual %s", testElement1Name, actual.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
123
internal/metrics/metrics.go
Normal file
123
internal/metrics/metrics.go
Normal file
@@ -0,0 +1,123 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2018, 2023
|
||||
|
||||
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 metrics contains code to provide metrics for the queue manager
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/internal/ready"
|
||||
"github.com/ibm-messaging/mq-container/pkg/logger"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = "9157"
|
||||
)
|
||||
|
||||
var (
|
||||
metricsEnabled = false
|
||||
// #nosec G112 - this code is changing soon to use https.
|
||||
// for now we will ignore the gosec.
|
||||
metricsServer = &http.Server{Addr: ":" + defaultPort}
|
||||
)
|
||||
|
||||
// GatherMetrics gathers metrics for the queue manager
|
||||
func GatherMetrics(qmName string, log *logger.Logger) {
|
||||
|
||||
// If running in standby mode - wait until the queue manager becomes active
|
||||
for {
|
||||
status, _ := ready.Status(context.Background(), qmName)
|
||||
if status.ActiveQM() {
|
||||
break
|
||||
}
|
||||
time.Sleep(requestTimeout * time.Second)
|
||||
}
|
||||
|
||||
metricsEnabled = true
|
||||
|
||||
err := startMetricsGathering(qmName, log)
|
||||
if err != nil {
|
||||
log.Errorf("Metrics Error: %s", err.Error())
|
||||
StopMetricsGathering(log)
|
||||
}
|
||||
}
|
||||
|
||||
// startMetricsGathering starts gathering metrics for the queue manager
|
||||
func startMetricsGathering(qmName string, log *logger.Logger) error {
|
||||
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Errorf("Metrics Error: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
log.Println("Starting metrics gathering")
|
||||
|
||||
// Start processing metrics
|
||||
go processMetrics(log, qmName)
|
||||
|
||||
// Wait for metrics to be ready before starting the Prometheus handler
|
||||
<-startChannel
|
||||
|
||||
// Register metrics
|
||||
metricsExporter := newExporter(qmName, log)
|
||||
err := prometheus.Register(metricsExporter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to register metrics: %v", err)
|
||||
}
|
||||
|
||||
// Setup HTTP server to handle requests from Prometheus
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(200)
|
||||
// #nosec G104
|
||||
w.Write([]byte("Status: METRICS ACTIVE"))
|
||||
})
|
||||
|
||||
go func() {
|
||||
err = metricsServer.ListenAndServe()
|
||||
if err != nil && err != http.ErrServerClosed {
|
||||
log.Errorf("Metrics Error: Failed to handle metrics request: %v", err)
|
||||
StopMetricsGathering(log)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// StopMetricsGathering stops gathering metrics for the queue manager
|
||||
func StopMetricsGathering(log *logger.Logger) {
|
||||
|
||||
if metricsEnabled {
|
||||
|
||||
// Stop processing metrics
|
||||
stopChannel <- true
|
||||
|
||||
// Shutdown HTTP server
|
||||
timeout, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
err := metricsServer.Shutdown(timeout)
|
||||
if err != nil {
|
||||
log.Errorf("Failed to shutdown metrics server: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
227
internal/metrics/update.go
Normal file
227
internal/metrics/update.go
Normal file
@@ -0,0 +1,227 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2018, 2019
|
||||
|
||||
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 metrics contains code to provide metrics for the queue manager
|
||||
package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/pkg/logger"
|
||||
"github.com/ibm-messaging/mq-golang/ibmmq"
|
||||
"github.com/ibm-messaging/mq-golang/mqmetric"
|
||||
)
|
||||
|
||||
const (
|
||||
qmgrLabelValue = mqmetric.QMgrMapKey
|
||||
requestTimeout = 10
|
||||
)
|
||||
|
||||
var (
|
||||
startChannel = make(chan bool)
|
||||
stopChannel = make(chan bool, 2)
|
||||
requestChannel = make(chan bool)
|
||||
responseChannel = make(chan map[string]*metricData)
|
||||
)
|
||||
|
||||
type metricData struct {
|
||||
name string
|
||||
description string
|
||||
objectType bool
|
||||
values map[string]float64
|
||||
isDelta bool
|
||||
}
|
||||
|
||||
// processMetrics processes publications of metric data and handles describe/collect/stop requests
|
||||
func processMetrics(log *logger.Logger, qmName string) {
|
||||
|
||||
var err error
|
||||
var firstConnect = true
|
||||
var metrics map[string]*metricData
|
||||
|
||||
for {
|
||||
// Connect to queue manager and discover available metrics
|
||||
err = doConnect(qmName)
|
||||
if err == nil {
|
||||
if firstConnect {
|
||||
firstConnect = false
|
||||
startChannel <- true
|
||||
}
|
||||
// #nosec G104
|
||||
metrics, _ = initialiseMetrics(log)
|
||||
}
|
||||
|
||||
// Now loop until something goes wrong
|
||||
for err == nil {
|
||||
|
||||
// Process publications of metric data
|
||||
// TODO: If we have a large number of metrics to process, then we could be blocked from responding to stop requests
|
||||
err = mqmetric.ProcessPublications()
|
||||
|
||||
// Handle describe/collect/stop requests
|
||||
if err == nil {
|
||||
select {
|
||||
case collect := <-requestChannel:
|
||||
if collect {
|
||||
updateMetrics(metrics)
|
||||
}
|
||||
responseChannel <- metrics
|
||||
case <-stopChannel:
|
||||
log.Println("Stopping metrics gathering")
|
||||
mqmetric.EndConnection()
|
||||
return
|
||||
case <-time.After(requestTimeout * time.Second):
|
||||
log.Debugf("Metrics: No requests received within timeout period (%d seconds)", requestTimeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Errorf("Metrics Error: %s", err.Error())
|
||||
|
||||
// Close the connection
|
||||
mqmetric.EndConnection()
|
||||
|
||||
// Handle stop requests
|
||||
select {
|
||||
case <-stopChannel:
|
||||
log.Println("Stopping metrics gathering")
|
||||
return
|
||||
case <-time.After(requestTimeout * time.Second):
|
||||
log.Println("Retrying metrics gathering")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// doConnect connects to the queue manager and discovers available metrics
|
||||
func doConnect(qmName string) error {
|
||||
|
||||
// Set connection configuration
|
||||
var connConfig mqmetric.ConnectionConfig
|
||||
connConfig.ClientMode = false
|
||||
connConfig.UserId = ""
|
||||
connConfig.Password = ""
|
||||
|
||||
// Connect to the queue manager - open the command and dynamic reply queues
|
||||
err := mqmetric.InitConnectionStats(qmName, "SYSTEM.DEFAULT.MODEL.QUEUE", "", &connConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to connect to queue manager %s: %v", qmName, err)
|
||||
}
|
||||
|
||||
// Discover available metrics for the queue manager and subscribe to them
|
||||
err = mqmetric.DiscoverAndSubscribe("", true, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to discover and subscribe to metrics: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initialiseMetrics sets initial details for all available metrics
|
||||
func initialiseMetrics(log *logger.Logger) (map[string]*metricData, error) {
|
||||
|
||||
metrics := make(map[string]*metricData)
|
||||
validMetrics := true
|
||||
metricNamesMap := generateMetricNamesMap()
|
||||
|
||||
for _, metricClass := range mqmetric.Metrics.Classes {
|
||||
for _, metricType := range metricClass.Types {
|
||||
if !strings.Contains(metricType.ObjectTopic, "%s") {
|
||||
for _, metricElement := range metricType.Elements {
|
||||
|
||||
// Get unique metric key
|
||||
key := makeKey(metricElement)
|
||||
|
||||
// Get metric name from mapping
|
||||
if metricLookup, found := metricNamesMap[key]; found {
|
||||
|
||||
// Check if metric is enabled
|
||||
if metricLookup.enabled {
|
||||
|
||||
// Check if metric is a delta type
|
||||
isDelta := false
|
||||
if metricElement.Datatype == ibmmq.MQIAMO_MONITOR_DELTA {
|
||||
isDelta = true
|
||||
}
|
||||
|
||||
// Set metric details
|
||||
metric := metricData{
|
||||
name: metricLookup.name,
|
||||
description: metricElement.Description,
|
||||
isDelta: isDelta,
|
||||
}
|
||||
|
||||
// Add metric
|
||||
if _, exists := metrics[key]; !exists {
|
||||
metrics[key] = &metric
|
||||
} else {
|
||||
log.Errorf("Metrics Error: Found duplicate metric key [%s]", key)
|
||||
validMetrics = false
|
||||
}
|
||||
} else {
|
||||
log.Debugf("Metrics: Skipping metric, metric is not enabled for key [%s]", key)
|
||||
}
|
||||
} else {
|
||||
log.Errorf("Metrics Error: Skipping metric, unexpected key [%s]", key)
|
||||
validMetrics = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !validMetrics {
|
||||
return metrics, fmt.Errorf("Invalid metrics data")
|
||||
}
|
||||
return metrics, nil
|
||||
}
|
||||
|
||||
// updateMetrics updates values for all available metrics
|
||||
func updateMetrics(metrics map[string]*metricData) {
|
||||
|
||||
for _, metricClass := range mqmetric.Metrics.Classes {
|
||||
for _, metricType := range metricClass.Types {
|
||||
if !strings.Contains(metricType.ObjectTopic, "%s") {
|
||||
for _, metricElement := range metricType.Elements {
|
||||
|
||||
// Unexpected metric elements (with no defined mapping) are handled in 'initialiseMetrics'
|
||||
// - if any exist, they are logged as errors and skipped (they are not added to the metrics map)
|
||||
// Therefore we can ignore handling any unexpected metric elements found here
|
||||
// - this avoids us logging excessive errors, as this function is called frequently
|
||||
metric, ok := metrics[makeKey(metricElement)]
|
||||
if ok {
|
||||
// Clear existing metric values
|
||||
metric.values = make(map[string]float64)
|
||||
|
||||
// Update metric with cached values of publication data
|
||||
for label, value := range metricElement.Values {
|
||||
normalisedValue := mqmetric.Normalise(metricElement, label, value)
|
||||
metric.values[label] = normalisedValue
|
||||
}
|
||||
}
|
||||
|
||||
// Reset cached values of publication data for this metric
|
||||
metricElement.Values = make(map[string]int64)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// makeKey builds a unique key for each metric
|
||||
func makeKey(metricElement *mqmetric.MonElement) string {
|
||||
return metricElement.Parent.Parent.Name + "/" + metricElement.Parent.Name + "/" + metricElement.Description
|
||||
}
|
||||
197
internal/metrics/update_test.go
Normal file
197
internal/metrics/update_test.go
Normal file
@@ -0,0 +1,197 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2018, 2019
|
||||
|
||||
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 metrics
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/pkg/logger"
|
||||
"github.com/ibm-messaging/mq-golang/mqmetric"
|
||||
)
|
||||
|
||||
const (
|
||||
testClassName = "CPU"
|
||||
testTypeName = "SystemSummary"
|
||||
testElement1Name = "cpu_load_five_minute_average_percentage"
|
||||
testElement2Name = "cpu_load_fifteen_minute_average_percentage"
|
||||
testElement1Description = "CPU load - five minute average"
|
||||
testElement2Description = "CPU load - fifteen minute average"
|
||||
testKey1 = testClassName + "/" + testTypeName + "/" + testElement1Description
|
||||
testKey2 = testClassName + "/" + testTypeName + "/" + testElement2Description
|
||||
)
|
||||
|
||||
func TestInitialiseMetrics(t *testing.T) {
|
||||
|
||||
teardownTestCase := setupTestCase(false)
|
||||
defer teardownTestCase()
|
||||
|
||||
metrics, err := initialiseMetrics(getTestLogger())
|
||||
metric, ok := metrics[testKey1]
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %s", err.Error())
|
||||
}
|
||||
if !ok {
|
||||
t.Error("Expected metric not found in map")
|
||||
} else {
|
||||
if metric.name != testElement1Name {
|
||||
t.Errorf("Expected name=%s; actual %s", testElement1Name, metric.name)
|
||||
}
|
||||
if metric.description != testElement1Description {
|
||||
t.Errorf("Expected description=%s; actual %s", testElement1Description, metric.description)
|
||||
}
|
||||
if metric.objectType != false {
|
||||
t.Errorf("Expected objectType=%v; actual %v", false, metric.objectType)
|
||||
}
|
||||
if len(metric.values) != 0 {
|
||||
t.Errorf("Expected values-size=%d; actual %d", 0, len(metric.values))
|
||||
}
|
||||
}
|
||||
_, ok = metrics[testKey2]
|
||||
if ok {
|
||||
t.Errorf("Unexpected metric found in map, %%s object topics should be ignored")
|
||||
}
|
||||
|
||||
if len(metrics) != 1 {
|
||||
t.Errorf("Map contains unexpected metrics, map size=%d", len(metrics))
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitialiseMetrics_UnexpectedKey(t *testing.T) {
|
||||
|
||||
teardownTestCase := setupTestCase(false)
|
||||
defer teardownTestCase()
|
||||
|
||||
mqmetric.Metrics.Classes[0].Types[0].Elements[0].Description = "New Metric"
|
||||
_, err := initialiseMetrics(getTestLogger())
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected skipping metric error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitialiseMetrics_DuplicateKeys(t *testing.T) {
|
||||
|
||||
teardownTestCase := setupTestCase(true)
|
||||
defer teardownTestCase()
|
||||
|
||||
_, err := initialiseMetrics(getTestLogger())
|
||||
|
||||
if err == nil {
|
||||
t.Error("Expected duplicate keys error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateMetrics(t *testing.T) {
|
||||
|
||||
teardownTestCase := setupTestCase(false)
|
||||
defer teardownTestCase()
|
||||
|
||||
metrics, _ := initialiseMetrics(getTestLogger())
|
||||
updateMetrics(metrics)
|
||||
|
||||
metric, _ := metrics[testKey1]
|
||||
actual, ok := metric.values[qmgrLabelValue]
|
||||
|
||||
if !ok {
|
||||
t.Error("No metric values found for queue manager label")
|
||||
} else {
|
||||
if actual != float64(1) {
|
||||
t.Errorf("Expected metric value=%f; actual %f", float64(1), actual)
|
||||
}
|
||||
if len(metric.values) != 1 {
|
||||
t.Errorf("Expected values-size=%d; actual %d", 1, len(metric.values))
|
||||
}
|
||||
}
|
||||
|
||||
if len(mqmetric.Metrics.Classes[0].Types[0].Elements[0].Values) != 0 {
|
||||
t.Error("Unexpected cached value; publication data should have been reset")
|
||||
}
|
||||
|
||||
updateMetrics(metrics)
|
||||
|
||||
if len(metric.values) != 0 {
|
||||
t.Errorf("Unexpected metric value; data should have been cleared")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMakeKey(t *testing.T) {
|
||||
|
||||
teardownTestCase := setupTestCase(false)
|
||||
defer teardownTestCase()
|
||||
|
||||
expected := testKey1
|
||||
actual := makeKey(mqmetric.Metrics.Classes[0].Types[0].Elements[0])
|
||||
if actual != expected {
|
||||
t.Errorf("Expected value=%s; actual %s", expected, actual)
|
||||
}
|
||||
}
|
||||
|
||||
func setupTestCase(duplicateKey bool) func() {
|
||||
populateTestMetrics(1, duplicateKey)
|
||||
return func() {
|
||||
cleanTestMetrics()
|
||||
}
|
||||
}
|
||||
|
||||
func populateTestMetrics(testValue int, duplicateKey bool) {
|
||||
|
||||
metricClass := new(mqmetric.MonClass)
|
||||
metricType1 := new(mqmetric.MonType)
|
||||
metricType2 := new(mqmetric.MonType)
|
||||
metricElement1 := new(mqmetric.MonElement)
|
||||
metricElement2 := new(mqmetric.MonElement)
|
||||
|
||||
metricClass.Name = testClassName
|
||||
metricType1.Name = testTypeName
|
||||
metricType2.Name = testTypeName
|
||||
metricElement1.MetricName = "Element1Name"
|
||||
metricElement1.Description = testElement1Description
|
||||
metricElement1.Values = make(map[string]int64)
|
||||
metricElement1.Values[qmgrLabelValue] = int64(testValue)
|
||||
metricElement2.MetricName = "Element2Name"
|
||||
metricElement2.Description = testElement2Description
|
||||
metricElement2.Values = make(map[string]int64)
|
||||
metricType1.ObjectTopic = "ObjectTopic"
|
||||
metricType2.ObjectTopic = "%s"
|
||||
metricElement1.Parent = metricType1
|
||||
metricElement2.Parent = metricType2
|
||||
metricType1.Parent = metricClass
|
||||
metricType2.Parent = metricClass
|
||||
|
||||
metricType1.Elements = make(map[int]*mqmetric.MonElement)
|
||||
metricType2.Elements = make(map[int]*mqmetric.MonElement)
|
||||
metricType1.Elements[0] = metricElement1
|
||||
if duplicateKey {
|
||||
metricType1.Elements[1] = metricElement1
|
||||
}
|
||||
metricType2.Elements[0] = metricElement2
|
||||
metricClass.Types = make(map[int]*mqmetric.MonType)
|
||||
metricClass.Types[0] = metricType1
|
||||
metricClass.Types[1] = metricType2
|
||||
mqmetric.Metrics.Classes = make(map[int]*mqmetric.MonClass)
|
||||
mqmetric.Metrics.Classes[0] = metricClass
|
||||
}
|
||||
|
||||
func cleanTestMetrics() {
|
||||
mqmetric.Metrics.Classes = make(map[int]*mqmetric.MonClass)
|
||||
}
|
||||
|
||||
func getTestLogger() *logger.Logger {
|
||||
log, _ := logger.NewLogger(os.Stdout, false, false, "test")
|
||||
return log
|
||||
}
|
||||
264
internal/mqscredact/mqscredact.go
Normal file
264
internal/mqscredact/mqscredact.go
Normal file
@@ -0,0 +1,264 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2019
|
||||
|
||||
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 mqscredact
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
/* List of sensitive MQ Parameters */
|
||||
var sensitiveParameters = []string{"LDAPPWD", "PASSWORD", "SSLCRYP"}
|
||||
|
||||
// redactionString is what sensitive paramters will be replaced with
|
||||
const redactionString = "(*********)"
|
||||
|
||||
func findEndOfParamterString(stringDenoter rune, r *bufio.Reader) string {
|
||||
parameter := ""
|
||||
for {
|
||||
char, _, err := r.ReadRune()
|
||||
if err != nil {
|
||||
return parameter
|
||||
}
|
||||
parameter = parameter + string(char)
|
||||
if char == stringDenoter {
|
||||
break
|
||||
} else if char == '\n' {
|
||||
// Check if we're on a comment line
|
||||
NewLineLoop:
|
||||
for {
|
||||
// Look at next character without moving buffer forwards
|
||||
chars, err := r.Peek(1)
|
||||
if err != nil {
|
||||
return parameter
|
||||
}
|
||||
// Check if we're at the beginning of some data.
|
||||
startOutput, _ := regexp.MatchString(`[^:0-9\s]`, string(chars[0]))
|
||||
if startOutput {
|
||||
// We are at the start, check if we're on a comment line
|
||||
if chars[0] == '*' {
|
||||
// found a comment line. go to the next newline chraracter
|
||||
CommentLoop:
|
||||
for {
|
||||
char, _, err = r.ReadRune()
|
||||
if err != nil {
|
||||
return parameter
|
||||
}
|
||||
parameter = parameter + string(char)
|
||||
if char == '\n' {
|
||||
break CommentLoop
|
||||
}
|
||||
}
|
||||
// Go round again as we're now on a new line
|
||||
continue NewLineLoop
|
||||
}
|
||||
// We've checked for comment and it isn't a comment line so break without moving buffer forwards
|
||||
break NewLineLoop
|
||||
}
|
||||
// Move the buffer forward and try again
|
||||
char, _, _ = r.ReadRune()
|
||||
parameter = parameter + string(char)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return parameter
|
||||
}
|
||||
|
||||
// getParameterString reads from r in order to find the end of the MQSC Parameter value. This is enclosed in ( ).
|
||||
// This function will return what it finds and will increment the reader pointer along as it goes.
|
||||
func getParameterString(r *bufio.Reader) string {
|
||||
// Add the ( in as it will have been dropped before.
|
||||
parameter := "("
|
||||
Loop:
|
||||
for {
|
||||
char, _, err := r.ReadRune()
|
||||
if err != nil {
|
||||
return parameter
|
||||
}
|
||||
|
||||
parameter = parameter + string(char)
|
||||
|
||||
switch char {
|
||||
case ')':
|
||||
break Loop
|
||||
// TODO: Duplicate code..
|
||||
case '\'', '"':
|
||||
parameter = parameter + findEndOfParamterString(char, r)
|
||||
}
|
||||
}
|
||||
|
||||
return parameter
|
||||
}
|
||||
|
||||
func resetAllParameters(currentVerb, originalString *string, lineContinuation, foundGap, parameterNext, redacting, checkComment *bool) {
|
||||
*currentVerb = ""
|
||||
*originalString = ""
|
||||
*lineContinuation = false
|
||||
*foundGap = false
|
||||
*parameterNext = false
|
||||
*redacting = false
|
||||
*checkComment = true
|
||||
}
|
||||
|
||||
// Redact is the main function for redacting sensitive parameters in MQSC strings
|
||||
// It accepts a string and redacts sensitive paramters such as LDAPPWD or PASSWORD
|
||||
func Redact(out string) (string, error) {
|
||||
out = strings.TrimSpace(out)
|
||||
var returnStr, currentVerb, originalString string
|
||||
var lineContinuation, foundGap, parameterNext, redacting, checkComment bool
|
||||
newline := true
|
||||
resetAllParameters(¤tVerb, &originalString, &lineContinuation, &foundGap, ¶meterNext, &redacting, &checkComment)
|
||||
r := bufio.NewReader(strings.NewReader(out))
|
||||
|
||||
MainLoop:
|
||||
for {
|
||||
// We have found a opening ( so use special parameter parsing
|
||||
if parameterNext {
|
||||
parameterStr := getParameterString(r)
|
||||
if !redacting {
|
||||
returnStr = returnStr + parameterStr
|
||||
} else {
|
||||
returnStr = returnStr + redactionString
|
||||
}
|
||||
|
||||
resetAllParameters(¤tVerb, &originalString, &lineContinuation, &foundGap, ¶meterNext, &redacting, &checkComment)
|
||||
}
|
||||
|
||||
// Loop round getting hte next parameter
|
||||
char, _, err := r.ReadRune()
|
||||
if err == io.EOF {
|
||||
if originalString != "" {
|
||||
returnStr = returnStr + originalString
|
||||
}
|
||||
break
|
||||
} else if err != nil {
|
||||
return returnStr, err
|
||||
}
|
||||
|
||||
/* We need to push forward until we find a non-whitespace, digit or colon character */
|
||||
if newline {
|
||||
startOutput, _ := regexp.MatchString(`[^:0-9\s]`, string(char))
|
||||
if !startOutput {
|
||||
originalString = originalString + string(char)
|
||||
continue MainLoop
|
||||
}
|
||||
newline = false
|
||||
}
|
||||
|
||||
switch char {
|
||||
// Found a line continuation character
|
||||
case '+', '-':
|
||||
lineContinuation = true
|
||||
foundGap = false
|
||||
originalString = originalString + string(char)
|
||||
continue MainLoop
|
||||
|
||||
// Found whitespace/new line
|
||||
case '\n':
|
||||
checkComment = true
|
||||
newline = true
|
||||
fallthrough
|
||||
case '\t', '\r', ' ':
|
||||
if !lineContinuation {
|
||||
foundGap = true
|
||||
}
|
||||
originalString = originalString + string(char)
|
||||
continue MainLoop
|
||||
|
||||
// Found a paramter value
|
||||
case '(':
|
||||
parameterNext = true
|
||||
/* Do not continue as we need to do some checks */
|
||||
|
||||
// Found a comment, parse in a special manner
|
||||
case '*':
|
||||
if checkComment {
|
||||
originalString = originalString + string(char)
|
||||
// Loop round until we find the new line character that marks the end of the comment
|
||||
CommentLoop:
|
||||
for {
|
||||
char, _, err := r.ReadRune()
|
||||
if err == io.EOF {
|
||||
if originalString != "" {
|
||||
returnStr = returnStr + originalString
|
||||
}
|
||||
break MainLoop
|
||||
} else if err != nil {
|
||||
return returnStr, err
|
||||
}
|
||||
originalString = originalString + string(char)
|
||||
|
||||
if char == '\n' {
|
||||
break CommentLoop
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
//Comment has been read and added to original string, go back to start
|
||||
checkComment = true
|
||||
newline = true
|
||||
continue MainLoop
|
||||
}
|
||||
/* Do not continue as we need to do some checks */
|
||||
|
||||
} //end of switch
|
||||
|
||||
checkComment = false
|
||||
|
||||
if lineContinuation {
|
||||
lineContinuation = false
|
||||
}
|
||||
if foundGap || parameterNext {
|
||||
// we've completed an parameter so check whether it is sensitive
|
||||
currentVerb = strings.ToUpper(currentVerb)
|
||||
|
||||
if isSensitiveCommand(currentVerb) {
|
||||
redacting = true
|
||||
}
|
||||
|
||||
// Add the unedited string to the return string
|
||||
returnStr = returnStr + originalString
|
||||
|
||||
//reset some of the parameters
|
||||
originalString = ""
|
||||
currentVerb = ""
|
||||
foundGap = false
|
||||
lineContinuation = false
|
||||
}
|
||||
|
||||
originalString = originalString + string(char)
|
||||
currentVerb = currentVerb + string(char)
|
||||
}
|
||||
|
||||
return returnStr, nil
|
||||
}
|
||||
|
||||
// isSensitiveCommand checks whether the given string contains a sensitive parameter.
|
||||
// We use contains here because we can't determine whether a line continuation seperates
|
||||
// parts of a parameter or two different parameters.
|
||||
func isSensitiveCommand(command string) bool {
|
||||
for _, v := range sensitiveParameters {
|
||||
if strings.Contains(command, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
171
internal/mqscredact/mqscredact_test.go
Normal file
171
internal/mqscredact/mqscredact_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2019
|
||||
|
||||
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 mqscredact
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
const passwordString = passwordHalf1 + passwordHalf2
|
||||
const passwordHalf1 = "hippo"
|
||||
const passwordHalf2 = "123456"
|
||||
|
||||
var testStrings = [...]string{
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD('" + passwordString + "')",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordString + "\")",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD ('" + passwordString + "')",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD\t\t('" + passwordString + "')",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) ldappwd('" + passwordString + "')",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LdApPwD('" + passwordString + "')",
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD('" + passwordString + "')",
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD(\"" + passwordString + "\")",
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD ('" + passwordString + "')",
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD\t\t('" + passwordString + "')",
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) password('" + passwordString + "')",
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) pAsSwOrD('" + passwordString + "')",
|
||||
"ALTER QMGR SSLCRYP('" + passwordString + "')",
|
||||
"ALTER QMGR SSLCRYP(\"" + passwordString + "\")",
|
||||
"ALTER QMGR SSLCRYP ('" + passwordString + "')",
|
||||
"ALTER QMGR SSLCRYP\t\t('" + passwordString + "')",
|
||||
"ALTER QMGR sslcryp('" + passwordString + "')",
|
||||
"ALTER QMGR sslCRYP('" + passwordString + "')",
|
||||
|
||||
// Line continuation ones
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "+\n " + passwordHalf2 + "\")",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "+\n\t" + passwordHalf2 + "\")",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "+\n\t " + passwordHalf2 + "\")",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD('" + passwordHalf1 + "+\n " + passwordHalf2 + "')",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD('" + passwordHalf1 + "+\n\t" + passwordHalf2 + "')",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD('" + passwordHalf1 + "+\n\t " + passwordHalf2 + "')",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "-\n" + passwordHalf2 + "\")",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD('" + passwordHalf1 + "-\n" + passwordHalf2 + "')",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "+ \n " + passwordHalf2 + "\")",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "+\t\n " + passwordHalf2 + "\")",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "- \n" + passwordHalf2 + "\")",
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD(\"" + passwordHalf1 + "-\t\n" + passwordHalf2 + "\")",
|
||||
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD(\"" + passwordHalf1 + "+\n " + passwordHalf2 + "\")",
|
||||
|
||||
"ALTER QMGR SSLCRYP(\"" + passwordHalf1 + "+\n " + passwordHalf2 + "\")",
|
||||
|
||||
//edge cases
|
||||
"ALTER QMGR SSLCRYP(\"" + passwordHalf1 + "+\n 123+\n 456\")",
|
||||
"ALTER QMGR SSLCRYP(\"" + passwordHalf1 + "-\n123-\n456\")",
|
||||
|
||||
"ALTER QMGR SSLCRYP(\"" + passwordHalf1 + "+\n 1+\n 2+\n 3+\n 4+\n 5+\n 6\")",
|
||||
"ALTER QMGR SSLCRYP(\"" + passwordHalf1 + "-\n1-\n2-\n3-\n4-\n5-\n6\")",
|
||||
|
||||
"ALTER QMGR SSLCRYP + \n (\"" + passwordHalf1 + "+\n 1+\n 2+\n 3+\n 4+\n 5+\n 6\")",
|
||||
"ALTER QMGR SSLCRYP - \n(\"" + passwordHalf1 + "-\n1-\n2-\n3-\n4-\n5-\n6\")",
|
||||
|
||||
"ALTER QMGR SSL + \n CRYP(\"" + passwordHalf1 + "+\n 1+\n 2+\n 3+\n 4+\n 5+\n 6\")",
|
||||
"ALTER QMGR SSL - \nCRYP(\"" + passwordHalf1 + "-\n1-\n2-\n3-\n4-\n5-\n6\")",
|
||||
|
||||
"ALTER QMGR + \n SSL +\n CRYP(\"" + passwordHalf1 + "+\n 1+\n 2+\n 3+\n 4+\n 5+\n 6\") +\n TEST(1234)",
|
||||
"ALTER QMGR -\nSSL -\nCRYP(\"" + passwordHalf1 + "-\n1-\n2-\n3-\n4-\n5-\n6\") -\nTEST(1234)",
|
||||
|
||||
"ALTER QMGR +\n * COMMENT\n SSL +\n * COMMENT IN MIDDLE\n CRYP('" + passwordString + "')",
|
||||
|
||||
" 1: ALTER CHANNEL(TEST2) CHLTYPE(SDR) PASS+\n : *test comment\n : WORD('" + passwordString + "')",
|
||||
" 2: ALTER CHANNEL(TEST3) CHLTYPE(SDR) PASSWORD('" + passwordHalf1 + "-\n*commentinmiddle with ' \n" + passwordHalf2 + "')",
|
||||
" 3: ALTER CHANNEL(TEST3) CHLTYPE(SDR) PASSWORD('" + passwordHalf1 + "-\n*commentinmiddle with ') \n" + passwordHalf2 + "')",
|
||||
}
|
||||
|
||||
var expected = [...]string{
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD " + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD\t\t" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) ldappwd" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LdApPwD" + redactionString,
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD" + redactionString,
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD" + redactionString,
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD " + redactionString,
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD\t\t" + redactionString,
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) password" + redactionString,
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) pAsSwOrD" + redactionString,
|
||||
"ALTER QMGR SSLCRYP" + redactionString,
|
||||
"ALTER QMGR SSLCRYP" + redactionString,
|
||||
"ALTER QMGR SSLCRYP " + redactionString,
|
||||
"ALTER QMGR SSLCRYP\t\t" + redactionString,
|
||||
"ALTER QMGR sslcryp" + redactionString,
|
||||
"ALTER QMGR sslCRYP" + redactionString,
|
||||
|
||||
// Line continuation ones
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
"DEFINE AUTHINFO(TEST) AUTHTYPE(IDPWLDAP) LDAPPWD" + redactionString,
|
||||
|
||||
"DEFINE CHANNEL(CHL) CHLTYPE(SOMETHING) PASSWORD" + redactionString,
|
||||
|
||||
"ALTER QMGR SSLCRYP" + redactionString,
|
||||
|
||||
//edge cases
|
||||
"ALTER QMGR SSLCRYP" + redactionString,
|
||||
"ALTER QMGR SSLCRYP" + redactionString,
|
||||
|
||||
"ALTER QMGR SSLCRYP" + redactionString,
|
||||
"ALTER QMGR SSLCRYP" + redactionString,
|
||||
|
||||
"ALTER QMGR SSLCRYP + \n \t " + redactionString,
|
||||
"ALTER QMGR SSLCRYP - \n " + redactionString,
|
||||
|
||||
"ALTER QMGR SSL + \n CRYP" + redactionString,
|
||||
"ALTER QMGR SSL - \nCRYP" + redactionString,
|
||||
|
||||
"ALTER QMGR + \n SSL +\n CRYP" + redactionString + " +\n TEST(1234)",
|
||||
"ALTER QMGR -\nSSL -\nCRYP" + redactionString + " -\nTEST(1234)",
|
||||
|
||||
"ALTER QMGR +\n * COMMENT\n SSL +\n * COMMENT IN MIDDLE\n CRYP" + redactionString,
|
||||
|
||||
"1: ALTER CHANNEL(TEST2) CHLTYPE(SDR) PASS+\n : *test comment\n : WORD" + redactionString,
|
||||
"2: ALTER CHANNEL(TEST3) CHLTYPE(SDR) PASSWORD" + redactionString,
|
||||
"3: ALTER CHANNEL(TEST3) CHLTYPE(SDR) PASSWORD" + redactionString,
|
||||
}
|
||||
|
||||
// Returns true if the 2 strings are equal ignoring whitespace characters
|
||||
func compareIgnoreWhiteSpace(str1, str2 string) bool {
|
||||
whiteSpaces := [...]string{" ", "\t", "\n", "\r"}
|
||||
for _, w := range whiteSpaces {
|
||||
str1 = strings.Replace(str1, w, "", -1)
|
||||
str2 = strings.Replace(str2, w, "", -1)
|
||||
}
|
||||
|
||||
return str1 == str2
|
||||
}
|
||||
|
||||
func TestAll(t *testing.T) {
|
||||
for i, v := range testStrings {
|
||||
back, _ := Redact(v)
|
||||
if strings.Contains(back, passwordHalf1) || strings.Contains(back, passwordHalf2) || strings.Contains(back, passwordString) {
|
||||
t.Errorf("MAJOR FAIL[%d]: Found an instance of the password. ", i)
|
||||
}
|
||||
|
||||
if !compareIgnoreWhiteSpace(back, expected[i]) {
|
||||
t.Errorf("FAIL[%d]:\nGave :%s\nexpected:%s\ngot :%s", i, v, expected[i], back)
|
||||
}
|
||||
}
|
||||
}
|
||||
62
internal/mqtemplate/mqtemplate.go
Normal file
62
internal/mqtemplate/mqtemplate.go
Normal file
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2018, 2020
|
||||
|
||||
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 mqtemplate contains code to process template files
|
||||
package mqtemplate
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"text/template"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/pkg/logger"
|
||||
)
|
||||
|
||||
// ProcessTemplateFile takes a Go templateFile, and processes it with the
|
||||
// supplied data, writing to destFile
|
||||
func ProcessTemplateFile(templateFile, destFile string, data interface{}, log *logger.Logger) error {
|
||||
// Re-configure channel if app password not set
|
||||
t, err := template.ParseFiles(templateFile)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
dir := path.Dir(destFile)
|
||||
_, err = os.Stat(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// #nosec G301
|
||||
err = os.MkdirAll(dir, 0770)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// #nosec G302 G304 G306 - its a read by owner/s group, and pose no harm.
|
||||
f, err := os.OpenFile(destFile, os.O_CREATE|os.O_WRONLY, 0660)
|
||||
// #nosec G307 - local to this function, pose no harm.
|
||||
defer f.Close()
|
||||
err = t.Execute(f, data)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
97
internal/mqversion/mqversion.go
Normal file
97
internal/mqversion/mqversion.go
Normal file
@@ -0,0 +1,97 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2020
|
||||
|
||||
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 mqversion
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/internal/command"
|
||||
)
|
||||
|
||||
// Get will return the current MQ version
|
||||
func Get() (string, error) {
|
||||
mqVersion, _, err := command.Run("dspmqver", "-b", "-f", "2")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Error Getting MQ version: %v", err)
|
||||
}
|
||||
return strings.TrimSpace(mqVersion), nil
|
||||
}
|
||||
|
||||
// Compare returns an integer comparing two MQ version strings lexicographically. The result will be 0 if currentVersion==checkVersion, -1 if currentVersion < checkVersion, and +1 if currentVersion > checkVersion
|
||||
func Compare(checkVersion string) (int, error) {
|
||||
currentVersion, err := Get()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
currentVRMF, err := parseVRMF(currentVersion)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
compareVRMF, err := parseVRMF(checkVersion)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("failed to parse compare version: %w", err)
|
||||
}
|
||||
return currentVRMF.compare(*compareVRMF), nil
|
||||
}
|
||||
|
||||
type vrmf [4]int
|
||||
|
||||
func (v vrmf) String() string {
|
||||
return fmt.Sprintf("%d.%d.%d.%d", v[0], v[1], v[2], v[3])
|
||||
}
|
||||
|
||||
func (v vrmf) compare(to vrmf) int {
|
||||
for idx := 0; idx < 4; idx++ {
|
||||
if v[idx] < to[idx] {
|
||||
return -1
|
||||
}
|
||||
if v[idx] > to[idx] {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func parseVRMF(vrmfString string) (*vrmf, error) {
|
||||
versionParts := strings.Split(vrmfString, ".")
|
||||
if len(versionParts) != 4 {
|
||||
return nil, fmt.Errorf("incorrect number of parts to version string: expected 4, got %d", len(versionParts))
|
||||
}
|
||||
vmrfPartNames := []string{"version", "release", "minor", "fix"}
|
||||
parsed := vrmf{}
|
||||
for idx, value := range versionParts {
|
||||
partName := vmrfPartNames[idx]
|
||||
if value == "" {
|
||||
return nil, fmt.Errorf("empty %s found in VRMF", partName)
|
||||
}
|
||||
val, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("non-numeric %s found in VRMF", partName)
|
||||
}
|
||||
if val < 0 {
|
||||
return nil, fmt.Errorf("negative %s found in VRMF", partName)
|
||||
}
|
||||
if idx == 0 && val == 0 {
|
||||
return nil, fmt.Errorf("zero value for version not allowed")
|
||||
}
|
||||
parsed[idx] = val
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
147
internal/mqversion/mqversion_test.go
Normal file
147
internal/mqversion/mqversion_test.go
Normal file
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2020
|
||||
|
||||
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 mqversion
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCompareLower(t *testing.T) {
|
||||
checkVersion := "99.99.99.99"
|
||||
mqVersionCheck, err := Compare(checkVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to compare MQ versions: %v", err)
|
||||
}
|
||||
if mqVersionCheck != -1 {
|
||||
t.Errorf("MQ version compare result failed. Expected -1, Got %v", mqVersionCheck)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareHigher(t *testing.T) {
|
||||
checkVersion := "1.1.1.1"
|
||||
mqVersionCheck, err := Compare(checkVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to compare MQ versions: %v", err)
|
||||
}
|
||||
if mqVersionCheck != 1 {
|
||||
t.Errorf("MQ version compare result failed. Expected 1, Got %v", mqVersionCheck)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompareEqual(t *testing.T) {
|
||||
checkVersion, err := Get()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current MQ version: %v", err)
|
||||
}
|
||||
mqVersionCheck, err := Compare(checkVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to compare MQ versions: %v", err)
|
||||
}
|
||||
if mqVersionCheck != 0 {
|
||||
t.Errorf("MQ version compare result failed. Expected 0, Got %v", mqVersionCheck)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersionValid(t *testing.T) {
|
||||
checkVersion, err := Get()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get current MQ version: %v", err)
|
||||
}
|
||||
_, err = parseVRMF(checkVersion)
|
||||
if err != nil {
|
||||
t.Fatalf("Validation of MQ version failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidVRMF(t *testing.T) {
|
||||
validVRMFs := map[string]vrmf{
|
||||
"1.0.0.0": {1, 0, 0, 0},
|
||||
"10.0.0.0": {10, 0, 0, 0},
|
||||
"1.10.0.0": {1, 10, 0, 0},
|
||||
"1.0.10.0": {1, 0, 10, 0},
|
||||
"1.0.0.10": {1, 0, 0, 10},
|
||||
"999.998.997.996": {999, 998, 997, 996},
|
||||
}
|
||||
for test, expect := range validVRMFs {
|
||||
t.Run(test, func(t *testing.T) {
|
||||
parsed, err := parseVRMF(test)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpectedly failed to parse VRMF '%s': %s", test, err.Error())
|
||||
}
|
||||
if *parsed != expect {
|
||||
t.Fatalf("VRMF not parsed as expected. Expected '%v', got '%v'", parsed, expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidVRMF(t *testing.T) {
|
||||
invalidVRMFs := []string{
|
||||
"not-a-number",
|
||||
"9.8.7.string",
|
||||
"0.1.2.3",
|
||||
"1.0.0.-10",
|
||||
}
|
||||
for _, test := range invalidVRMFs {
|
||||
t.Run(test, func(t *testing.T) {
|
||||
parsed, err := parseVRMF(test)
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error when parsing VRMF '%s', but got none. VRMF returned: %v", test, parsed)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompare(t *testing.T) {
|
||||
tests := []struct {
|
||||
current string
|
||||
compare string
|
||||
expect int
|
||||
}{
|
||||
{"1.0.0.1", "1.0.0.1", 0},
|
||||
{"1.0.0.1", "1.0.0.0", 1},
|
||||
{"1.0.0.1", "1.0.0.2", -1},
|
||||
{"9.9.9.9", "10.0.0.0", -1},
|
||||
{"9.9.9.9", "9.10.0.0", -1},
|
||||
{"9.9.9.9", "9.9.10.0", -1},
|
||||
{"9.9.9.9", "9.9.9.10", -1},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%s-%s", test.current, test.compare), func(t *testing.T) {
|
||||
baseVRMF, err := parseVRMF(test.current)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not parse base version '%s': %s", test.current, err.Error())
|
||||
}
|
||||
compareVRMF, err := parseVRMF(test.compare)
|
||||
if err != nil {
|
||||
t.Fatalf("Could not parse current version '%s': %s", test.current, err.Error())
|
||||
}
|
||||
result := baseVRMF.compare(*compareVRMF)
|
||||
if result != test.expect {
|
||||
t.Fatalf("Expected %d but got %d when comparing '%s' with '%s'", test.expect, result, test.current, test.compare)
|
||||
}
|
||||
if test.expect == 0 {
|
||||
return
|
||||
}
|
||||
resultReversed := compareVRMF.compare(*baseVRMF)
|
||||
if resultReversed != test.expect*-1 {
|
||||
t.Fatalf("Expected %d but got %d when comparing '%s' with '%s'", test.expect*-1, resultReversed, test.compare, test.current)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
39
internal/pathutils/clean.go
Normal file
39
internal/pathutils/clean.go
Normal file
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2023
|
||||
|
||||
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 pathutils contains code to provide sanitised file paths
|
||||
package pathutils
|
||||
|
||||
import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// CleanPath returns the result of joining a series of sanitised file paths (preventing directory traversal for each path)
|
||||
// If the first path is relative, a relative path is returned
|
||||
func CleanPath(paths ...string) string {
|
||||
if len(paths) == 0 {
|
||||
return ""
|
||||
}
|
||||
var combined string
|
||||
if !path.IsAbs(paths[0]) {
|
||||
combined = "./"
|
||||
}
|
||||
for _, part := range paths {
|
||||
combined = filepath.Join(combined, filepath.FromSlash(path.Clean("/"+part)))
|
||||
}
|
||||
return combined
|
||||
}
|
||||
45
internal/pathutils/clean_test.go
Normal file
45
internal/pathutils/clean_test.go
Normal file
@@ -0,0 +1,45 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2023
|
||||
|
||||
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 pathutils
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestClean(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
uncleaned string
|
||||
filepath string
|
||||
cleaned string
|
||||
}{
|
||||
{"/a/rooted/path", "some.file", "/a/rooted/path/some.file"},
|
||||
{"../../../a/relative/path", "abc.txt", "a/relative/path/abc.txt"},
|
||||
{"a/path" + ".p12", "some.file", "a/path.p12/some.file"},
|
||||
{"/", "bin", "/bin"},
|
||||
{"abc/def", "../../a/relative/path", "abc/def/a/relative/path"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
cleaned := CleanPath(test.uncleaned, test.filepath)
|
||||
|
||||
if !strings.EqualFold(cleaned, test.cleaned) {
|
||||
t.Fatalf("file path sanitisation failed. Expected %s but got %s\n", test.cleaned, cleaned)
|
||||
}
|
||||
}
|
||||
}
|
||||
166
internal/ready/ready.go
Normal file
166
internal/ready/ready.go
Normal file
@@ -0,0 +1,166 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2018, 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 ready contains code to provide a ready signal mechanism between processes
|
||||
package ready
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/internal/command"
|
||||
)
|
||||
|
||||
const readyFile string = "/run/runmqserver/ready"
|
||||
const readyToSyncFile string = "/run/runmqserver/ready-to-sync"
|
||||
|
||||
func fileExists(fileName string) (bool, error) {
|
||||
_, err := os.Stat(fileName)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return false, err
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Clear ensures that any readiness state is cleared
|
||||
func Clear() error {
|
||||
err := clearFile(readyFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = clearFile(readyToSyncFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearFile removes the specified file if it exists
|
||||
func clearFile(fileName string) error {
|
||||
exist, err := fileExists(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
return os.Remove(fileName)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set lets any subsequent calls to `CheckReady` know that the queue
|
||||
// manager has finished its configuration step
|
||||
func Set() error {
|
||||
// #nosec G306 - this gives permissions to owner/s group only.
|
||||
return os.WriteFile(readyFile, []byte("1"), 0770)
|
||||
}
|
||||
|
||||
// Check checks whether or not the queue manager has finished its
|
||||
// configuration steps
|
||||
func Check() (bool, error) {
|
||||
exists, err := fileExists(readyFile)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return exists, nil
|
||||
}
|
||||
|
||||
// SetReadyToSync is used to indicate that a Native-HA queue manager instance is ready-to-sync
|
||||
func SetReadyToSync() error {
|
||||
|
||||
exists, err := fileExists(readyToSyncFile)
|
||||
if err != nil {
|
||||
return err
|
||||
} else if exists {
|
||||
return nil
|
||||
}
|
||||
|
||||
readyToSyncStartTime := strconv.FormatInt(time.Now().Unix(), 10)
|
||||
// #nosec G306 - required permissions
|
||||
return os.WriteFile(readyToSyncFile, []byte(readyToSyncStartTime), 0660)
|
||||
}
|
||||
|
||||
// GetReadyToSyncStartTime returns the start-time a Native-HA queue manager instance was ready-to-sync
|
||||
func GetReadyToSyncStartTime() (bool, time.Time, error) {
|
||||
|
||||
exists, err := fileExists(readyToSyncFile)
|
||||
if err != nil {
|
||||
return exists, time.Time{}, err
|
||||
}
|
||||
|
||||
if exists {
|
||||
buf, err := os.ReadFile(readyToSyncFile)
|
||||
if err != nil {
|
||||
return true, time.Time{}, err
|
||||
}
|
||||
readyToSyncStartTime, err := strconv.ParseInt(string(buf), 10, 64)
|
||||
if err != nil {
|
||||
return true, time.Time{}, err
|
||||
}
|
||||
return true, time.Unix(readyToSyncStartTime, 0), nil
|
||||
}
|
||||
|
||||
return false, time.Time{}, nil
|
||||
}
|
||||
|
||||
// Status returns an enum representing the current running status of the queue manager
|
||||
func Status(ctx context.Context, name string) (QMStatus, error) {
|
||||
out, _, err := command.RunContext(ctx, "dspmq", "-n", "-m", name)
|
||||
if err != nil {
|
||||
return StatusUnknown, err
|
||||
}
|
||||
if strings.Contains(string(out), "(RUNNING)") {
|
||||
return StatusActiveQM, nil
|
||||
}
|
||||
if strings.Contains(string(out), "(RUNNING AS STANDBY)") {
|
||||
return StatusStandbyQM, nil
|
||||
}
|
||||
if strings.Contains(string(out), "(REPLICA)") {
|
||||
return StatusStandbyQM, nil
|
||||
}
|
||||
if strings.Contains(string(out), "(RECOVERY GROUP LEADER)") {
|
||||
return StatusRecoveryQM, nil
|
||||
}
|
||||
return StatusUnknown, nil
|
||||
}
|
||||
|
||||
type QMStatus int
|
||||
|
||||
const (
|
||||
StatusUnknown QMStatus = iota
|
||||
StatusActiveQM
|
||||
StatusStandbyQM
|
||||
StatusReplicaQM
|
||||
StatusRecoveryQM
|
||||
)
|
||||
|
||||
// ActiveQM returns true if the queue manager is running in active mode
|
||||
func (s QMStatus) ActiveQM() bool { return s == StatusActiveQM }
|
||||
|
||||
// StandbyQM returns true if the queue manager is running in standby mode
|
||||
func (s QMStatus) StandbyQM() bool { return s == StatusStandbyQM }
|
||||
|
||||
// ReplicaQM returns true if the queue manager is running in replica mode
|
||||
func (s QMStatus) ReplicaQM() bool { return s == StatusReplicaQM }
|
||||
|
||||
// ReplicaQM returns true if the queue manager is running in recovery mode
|
||||
func (s QMStatus) RecoveryQM() bool { return s == StatusRecoveryQM }
|
||||
61
internal/securityutility/securityutility.go
Normal file
61
internal/securityutility/securityutility.go
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
© 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.
|
||||
*/
|
||||
|
||||
// Look for go standards for package comment
|
||||
// Package securityUtility contains code to use securityUtility tool from opt/mqm/web/bin directory
|
||||
// to encode the passwords
|
||||
package securityutility
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// EncodeSecrets takes a secret/password as an input and encodes the password using securityUtility
|
||||
// and returns the encoded password
|
||||
func EncodeSecrets(secret string) (string, error) {
|
||||
_, err := os.Stat("/opt/mqm/web/bin/securityUtility")
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return "", err
|
||||
}
|
||||
if len(secret) > 256 {
|
||||
return "", fmt.Errorf("length of password is greater than the maximum length of 256 characters, length of password is %d", len(secret))
|
||||
}
|
||||
// Set the java environment required for running securityUtility tool and then run the securityUtility tool
|
||||
// to encode the password using "aes" encoding
|
||||
// #nosec G204
|
||||
cmd := exec.Command("/bin/sh", "-c", "source setmqenv -s;/opt/mqm/web/bin/server; /opt/mqm/web/bin/securityUtility encode --encoding=aes "+secret)
|
||||
cmd.Env = os.Environ()
|
||||
cmd.Env = append(cmd.Env, "JAVA_HOME=/opt/mqm/java/jre64/jre")
|
||||
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
encodedSecret := ""
|
||||
cmdOutput := strings.Split(string(out), "\n")
|
||||
// When the JVM is in FIPS 140-2 mode and the IBMJCEPlusFIPS provider is used the following message is displayed
|
||||
// The IBMJCEPlusFIPS provider is configured for FIPS 140-2. Please note that the 140-2 configuration may be removed in the future.
|
||||
// Hence read only the encoded password and ignore the above message
|
||||
for _, line := range cmdOutput {
|
||||
if strings.Contains(line, "{aes}") {
|
||||
encodedSecret = line
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(encodedSecret), nil
|
||||
}
|
||||
135
internal/simpleauth/simpleauth.go
Normal file
135
internal/simpleauth/simpleauth.go
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2020, 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.
|
||||
*/
|
||||
|
||||
//This is a developer only configuration and not recommended for production usage.
|
||||
|
||||
package simpleauth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/internal/securityutility"
|
||||
"github.com/ibm-messaging/mq-container/pkg/logger"
|
||||
)
|
||||
|
||||
const MQ_APP_PWD_ENV = "MQ_APP_PASSWORD"
|
||||
const MQ_APP_PWD_SECURE_ENV = "MQ_APP_PASSWORD_SECURE"
|
||||
const MQ_ADMIN_PWD_ENV = "MQ_ADMIN_PASSWORD"
|
||||
const MQ_ADMIN_PWD_SECURE_ENV = "MQ_ADMIN_PASSWORD_SECURE"
|
||||
const MQ_CONNAUTH_USE_HTP_ENV = "MQ_CONNAUTH_USE_HTP"
|
||||
|
||||
// #nosec G101
|
||||
const MQ_APP_USER_SECRET_PATH = "/run/secrets/mqAppPassword"
|
||||
|
||||
// #nosec G101
|
||||
const MQ_ADMIN_USER_SECRET_PATH = "/run/secrets/mqAdminPassword"
|
||||
|
||||
// IsEnabled will return a boolean value if the MQ_CONNAUTH_USER_HTP_ENV is set to true and if the app/admin
|
||||
// user passwords are set as environment variables or set as secrets
|
||||
func IsEnabled() bool {
|
||||
mqSimpleAuthEnabled := false
|
||||
enableHtPwd, set := os.LookupEnv(MQ_CONNAUTH_USE_HTP_ENV)
|
||||
adminPassword, adminPwdSet := os.LookupEnv(MQ_ADMIN_PWD_ENV)
|
||||
adminSecret, adminSecretSet := os.LookupEnv(MQ_ADMIN_PWD_SECURE_ENV)
|
||||
appPassword, appPwdSet := os.LookupEnv(MQ_APP_PWD_ENV)
|
||||
appSecret, appSecretSet := os.LookupEnv(MQ_APP_PWD_SECURE_ENV)
|
||||
|
||||
if set && strings.EqualFold(enableHtPwd, "true") &&
|
||||
(adminPwdSet && len(strings.TrimSpace(adminPassword)) > 0 || appPwdSet && len(strings.TrimSpace(appPassword)) > 0 ||
|
||||
appSecretSet && len(strings.TrimSpace(appSecret)) > 0 || adminSecretSet && len(strings.TrimSpace(adminSecret)) > 0) {
|
||||
mqSimpleAuthEnabled = true
|
||||
}
|
||||
return mqSimpleAuthEnabled
|
||||
}
|
||||
|
||||
// CheckForPasswords checks if the user has provided the app & admin user passwords via the environment variable
|
||||
// or via the secrets. The secrets will be in /run/secrets path
|
||||
func CheckForPasswords(log *logger.Logger) error {
|
||||
adminPassword, adminPwdSet := os.LookupEnv(MQ_ADMIN_PWD_ENV)
|
||||
appPassword, appPwdSet := os.LookupEnv(MQ_APP_PWD_ENV)
|
||||
|
||||
if adminPwdSet && len(strings.TrimSpace(adminPassword)) > 0 {
|
||||
encodedAdminPassword, err := securityutility.EncodeSecrets(adminPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding Admin password for web server failed with error %v", err)
|
||||
}
|
||||
err = os.Setenv(MQ_ADMIN_PWD_SECURE_ENV, encodedAdminPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting encoded admin user password to environment variable failed with error %v", err)
|
||||
}
|
||||
log.Printf("Environment variable MQ_ADMIN_PASSWORD is deprecated, use secrets to set the passwords")
|
||||
} else {
|
||||
if _, err := os.Stat(MQ_ADMIN_USER_SECRET_PATH); err == nil {
|
||||
encodedAdminSecret, err := readMQSecrets(MQ_ADMIN_USER_SECRET_PATH)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding mqAdminPassword secret for web server failed with error %v", err)
|
||||
}
|
||||
if len(encodedAdminSecret) > 0 {
|
||||
err = os.Setenv(MQ_ADMIN_PWD_SECURE_ENV, encodedAdminSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting encoded admin user password to environment variable failed with error %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if appPwdSet && len(strings.TrimSpace(appPassword)) > 0 {
|
||||
encodedAppPassword, err := securityutility.EncodeSecrets(appPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding App password for web server failed with error %v", err)
|
||||
}
|
||||
err = os.Setenv(MQ_APP_PWD_SECURE_ENV, encodedAppPassword)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting encoded app user password to environment variable failed with error %v", err)
|
||||
}
|
||||
log.Printf("Environment variable MQ_APP_PASSWORD is deprecated, use secrets to set the passwords")
|
||||
} else {
|
||||
// If environment variables are not set check if secrets were used to set the passwords
|
||||
if _, err := os.Stat(MQ_APP_USER_SECRET_PATH); err == nil {
|
||||
encodedAppSecret, err := readMQSecrets(MQ_APP_USER_SECRET_PATH)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encoding mqAppPassword secret for web server failed with error %v", err)
|
||||
}
|
||||
if len(encodedAppSecret) > 0 {
|
||||
err := os.Setenv(MQ_APP_PWD_SECURE_ENV, encodedAppSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("setting encoded app user password to environment variable failed with error %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// readMQSecrets takes the secret file as an input and encodes the secret and returns an encoded password
|
||||
func readMQSecrets(secretName string) (string, error) {
|
||||
passwordBuf, err := os.ReadFile(filepath.Clean(secretName))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(passwordBuf) > 256 {
|
||||
err = fmt.Errorf("the length of the password cannot be more than 256 characters, length of the password was %v", len(passwordBuf))
|
||||
return "", err
|
||||
}
|
||||
encodedPassword, err := securityutility.EncodeSecrets(string(passwordBuf))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return encodedPassword, nil
|
||||
}
|
||||
745
internal/tls/tls.go
Normal file
745
internal/tls/tls.go
Normal file
@@ -0,0 +1,745 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2019, 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 tls
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
pwr "math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"crypto/sha512"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
|
||||
pkcs "software.sslmate.com/src/go-pkcs12"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/internal/keystore"
|
||||
"github.com/ibm-messaging/mq-container/internal/mqtemplate"
|
||||
"github.com/ibm-messaging/mq-container/internal/pathutils"
|
||||
"github.com/ibm-messaging/mq-container/pkg/logger"
|
||||
)
|
||||
|
||||
// cmsKeystoreName is the name of the CMS Keystore
|
||||
const cmsKeystoreName = "key.kdb"
|
||||
|
||||
// p12TruststoreName is the name of the PKCS#12 Truststore
|
||||
const p12TruststoreName = "trust.p12"
|
||||
|
||||
// keystoreDirDefault is the location for the default CMS Keystore & PKCS#12 Truststore
|
||||
const keystoreDirDefault = "/run/runmqserver/tls/"
|
||||
|
||||
// keystoreDirHA is the location for the HA CMS Keystore
|
||||
const keystoreDirHA = "/run/runmqserver/ha/tls/"
|
||||
|
||||
// keyDirDefault is the location of the default keys to import
|
||||
const keyDirDefault = "/etc/mqm/pki/keys"
|
||||
|
||||
// keyDirHA is the location of the HA keys to import
|
||||
const keyDirHA = "/etc/mqm/ha/pki/keys"
|
||||
|
||||
// keyDirGroupHA is the location of the GroupHA keys to import
|
||||
const keyDirGroupHA = "/etc/mqm/groupha/pki/keys"
|
||||
|
||||
// trustDirDefault is the location of the trust certificates to import
|
||||
const trustDirDefault = "/etc/mqm/pki/trust"
|
||||
|
||||
// trustDirGroupDefault is the location of the GroupHA trust certificates to import
|
||||
const trustDirGroupHA = "/etc/mqm/groupha/pki/trust"
|
||||
|
||||
type KeyStoreData struct {
|
||||
Keystore *keystore.KeyStore
|
||||
Password string
|
||||
TrustedCerts []*pem.Block
|
||||
KnownFingerPrints []string
|
||||
KeyLabels []string
|
||||
}
|
||||
|
||||
type P12KeyFiles struct {
|
||||
Keystores []string
|
||||
Password string
|
||||
}
|
||||
|
||||
type TLSStore struct {
|
||||
Keystore KeyStoreData
|
||||
Truststore KeyStoreData
|
||||
}
|
||||
|
||||
func configureTLSKeystores(keystoreDir string, keyDirs, trustDirs []string, p12TruststoreRequired bool, nativeTLSHA bool) ([]string, KeyStoreData, KeyStoreData, error) {
|
||||
var keyLabel string
|
||||
|
||||
cmsKeystoreRequired := false
|
||||
allDirs := append([]string{}, keyDirs...)
|
||||
allDirs = append(allDirs, trustDirs...)
|
||||
for _, dir := range allDirs {
|
||||
if haveKeysAndCerts(dir) {
|
||||
cmsKeystoreRequired = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// Create the CMS Keystore & PKCS#12 Truststore (if required)
|
||||
tlsStore, err := generateAllKeystores(keystoreDir, cmsKeystoreRequired, p12TruststoreRequired, nativeTLSHA)
|
||||
if err != nil {
|
||||
return nil, tlsStore.Keystore, tlsStore.Truststore, err
|
||||
}
|
||||
|
||||
keyLabels := make([]string, len(keyDirs))
|
||||
if tlsStore.Keystore.Keystore != nil {
|
||||
for idx, keyDir := range keyDirs {
|
||||
// Process all keys - add them to the CMS KeyStore
|
||||
keyLabel, err = processKeys(&tlsStore, keystoreDir, keyDir)
|
||||
if err != nil {
|
||||
return nil, tlsStore.Keystore, tlsStore.Truststore, err
|
||||
}
|
||||
keyLabels[idx] = keyLabel
|
||||
}
|
||||
}
|
||||
|
||||
for _, trustDir := range trustDirs {
|
||||
// Process all trust certificates - add them to the CMS KeyStore & PKCS#12 Truststore (if required)
|
||||
err = processTrustCertificates(&tlsStore, trustDir)
|
||||
if err != nil {
|
||||
return nil, tlsStore.Keystore, tlsStore.Truststore, err
|
||||
}
|
||||
}
|
||||
|
||||
return keyLabels, tlsStore.Keystore, tlsStore.Truststore, err
|
||||
}
|
||||
|
||||
// ConfigureDefaultTLSKeystores configures the CMS Keystore & PKCS#12 Truststore
|
||||
func ConfigureDefaultTLSKeystores() (string, KeyStoreData, KeyStoreData, error) {
|
||||
certLabels, keyStore, trustStore, err := configureTLSKeystores(keystoreDirDefault, []string{keyDirDefault}, []string{trustDirDefault}, true, false)
|
||||
if err != nil {
|
||||
return "", keyStore, trustStore, err
|
||||
}
|
||||
certLabel := ""
|
||||
if len(certLabels) > 0 {
|
||||
certLabel = certLabels[0]
|
||||
}
|
||||
return certLabel, keyStore, trustStore, err
|
||||
}
|
||||
|
||||
// ConfigureHATLSKeystore configures the CMS Keystore & PKCS#12 Truststore
|
||||
func ConfigureHATLSKeystore() (string, string, KeyStoreData, KeyStoreData, error) {
|
||||
// *.crt files mounted to the HA TLS dir keyDirHA will be processed as trusted in the CMS keystore
|
||||
keyDirs := []string{keyDirHA, keyDirGroupHA}
|
||||
haCertLabels, haKeystore, haTruststore, err := configureTLSKeystores(keystoreDirHA, keyDirs, keyDirs, false, true)
|
||||
if err != nil {
|
||||
return "", "", haKeystore, haTruststore, err
|
||||
}
|
||||
if len(haCertLabels) != len(keyDirs) {
|
||||
return "", "", haKeystore, haTruststore, fmt.Errorf("incorrect number of certificate labels returned (expected %d, got %d)", len(keyDirs), len(haCertLabels))
|
||||
}
|
||||
|
||||
return haCertLabels[0], haCertLabels[1], haKeystore, haTruststore, err
|
||||
}
|
||||
|
||||
// ConfigureTLS configures TLS for the queue manager
|
||||
func ConfigureTLS(keyLabel string, cmsKeystore KeyStoreData, devMode bool, log *logger.Logger) error {
|
||||
|
||||
const mqscLink string = "/run/15-tls.mqsc"
|
||||
const mqscTemplate string = "/etc/mqm/15-tls.mqsc.tpl"
|
||||
sslKeyRing := ""
|
||||
var fipsEnabled = "NO"
|
||||
|
||||
// Don't set SSLKEYR if no keys or crts are not supplied
|
||||
// Key label will be blank if no private keys were added during processing keys and certs.
|
||||
if cmsKeystore.Keystore != nil && len(keyLabel) > 0 {
|
||||
certList, _ := cmsKeystore.Keystore.ListAllCertificates()
|
||||
if len(certList) > 0 {
|
||||
sslKeyRing = strings.TrimSuffix(cmsKeystore.Keystore.Filename, ".kdb")
|
||||
}
|
||||
|
||||
if cmsKeystore.Keystore.IsFIPSEnabled() {
|
||||
fipsEnabled = "YES"
|
||||
}
|
||||
}
|
||||
err := mqtemplate.ProcessTemplateFile(mqscTemplate, mqscLink, map[string]string{
|
||||
"SSLKeyR": sslKeyRing,
|
||||
"CertificateLabel": keyLabel,
|
||||
"SSLFips": fipsEnabled,
|
||||
}, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if devMode && keyLabel != "" {
|
||||
err = configureTLSDev(log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// configureTLSDev configures TLS for the developer defaults
|
||||
func configureTLSDev(log *logger.Logger) error {
|
||||
|
||||
const mqscLink string = "/run/20-dev-tls.mqsc"
|
||||
const mqscTemplate string = "/etc/mqm/20-dev-tls.mqsc.tpl"
|
||||
|
||||
if os.Getenv("MQ_DEV") == "true" {
|
||||
err := mqtemplate.ProcessTemplateFile(mqscTemplate, mqscLink, map[string]string{}, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateAllKeystores creates the CMS Keystore & PKCS#12 Truststore (if required)
|
||||
func generateAllKeystores(keystoreDir string, createCMSKeystore bool, p12TruststoreRequired bool, nativeTLSHA bool) (TLSStore, error) {
|
||||
|
||||
var cmsKeystore, p12Truststore KeyStoreData
|
||||
|
||||
// Generate a pasword for use with both the CMS Keystore & PKCS#12 Truststore
|
||||
pw := generateRandomPassword()
|
||||
cmsKeystore.Password = pw
|
||||
p12Truststore.Password = pw
|
||||
|
||||
// Create the Keystore directory - if it does not already exist
|
||||
// #nosec G301 - write group permissions are required
|
||||
err := os.MkdirAll(keystoreDir, 0770)
|
||||
if err != nil {
|
||||
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create Keystore directory: %v", err)
|
||||
}
|
||||
|
||||
// Create the CMS Keystore if we have been provided keys and certificates
|
||||
if createCMSKeystore {
|
||||
cmsKeystore.Keystore = keystore.NewCMSKeyStore(pathutils.CleanPath(keystoreDir, cmsKeystoreName), cmsKeystore.Password)
|
||||
err = cmsKeystore.Keystore.Create()
|
||||
if err != nil {
|
||||
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create CMS Keystore: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Create the PKCS#12 Truststore (if required)
|
||||
if p12TruststoreRequired {
|
||||
p12Truststore.Keystore = keystore.NewPKCS12KeyStore(pathutils.CleanPath(keystoreDir, p12TruststoreName), p12Truststore.Password)
|
||||
err = p12Truststore.Keystore.Create()
|
||||
if err != nil {
|
||||
return TLSStore{cmsKeystore, p12Truststore}, fmt.Errorf("Failed to create PKCS#12 Truststore: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return TLSStore{cmsKeystore, p12Truststore}, nil
|
||||
}
|
||||
|
||||
// processKeys processes all keys - adding them to the CMS KeyStore
|
||||
func processKeys(tlsStore *TLSStore, keystoreDir string, keyDir string) (string, error) {
|
||||
|
||||
// Key label - will be set to the label of the first set of keys
|
||||
keyLabel := ""
|
||||
|
||||
// Process all keys
|
||||
keyList, err := os.ReadDir(keyDir)
|
||||
if err == nil && len(keyList) > 0 {
|
||||
// Process each set of keys - each set should contain files: *.key & *.crt
|
||||
for _, keySet := range keyList {
|
||||
keys, _ := os.ReadDir(pathutils.CleanPath(keyDir, keySet.Name()))
|
||||
|
||||
// Ensure the label of the set of keys does not match the name of the PKCS#12 Truststore
|
||||
if keySet.Name() == p12TruststoreName[0:len(p12TruststoreName)-len(filepath.Ext(p12TruststoreName))] {
|
||||
return "", fmt.Errorf("key label cannot be set to the Truststore name: %v", keySet.Name())
|
||||
}
|
||||
|
||||
// Process private key (*.key)
|
||||
privateKey, keyPrefix, err := processPrivateKey(keyDir, keySet.Name(), keys)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// If private key does not exist - skip this set of keys
|
||||
if privateKey == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Process certificates (*.crt) - public certificate & optional CA certificate
|
||||
publicCertificate, caCertificate, err := processCertificates(keyDir, keySet.Name(), keyPrefix, keys, &tlsStore.Keystore, &tlsStore.Truststore)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Return an error if corresponding public certificate was not found. Both private key and
|
||||
// it's corresponding public certificate are required.
|
||||
if publicCertificate == nil {
|
||||
return "", fmt.Errorf("Failed to find public certificate in directory %s", keyDir)
|
||||
}
|
||||
|
||||
// Validate certificates for duplicate Subject DNs
|
||||
if len(caCertificate) > 0 {
|
||||
errCertValid := validateCertificates(publicCertificate, caCertificate)
|
||||
if errCertValid != nil {
|
||||
return "", errCertValid
|
||||
}
|
||||
}
|
||||
// Create a new PKCS#12 Keystore - containing private key, public certificate & optional CA certificate
|
||||
file, err := pkcs.Modern.Encode(privateKey, publicCertificate, caCertificate, tlsStore.Keystore.Password)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to encode PKCS#12 Keystore %s: %v", keySet.Name()+".p12", err)
|
||||
}
|
||||
keystorePath := pathutils.CleanPath(keystoreDir, keySet.Name()+".p12")
|
||||
// #nosec G306 - this gives permissions to owner/s group only.
|
||||
err = os.WriteFile(keystorePath, file, 0644)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to write PKCS#12 Keystore %s: %v", keystorePath, err)
|
||||
}
|
||||
|
||||
// Import the new PKCS#12 Keystore into the CMS Keystore
|
||||
err = tlsStore.Keystore.Keystore.Import(keystorePath, tlsStore.Keystore.Password)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to import keys from %s into CMS Keystore: %v", keystorePath, err)
|
||||
}
|
||||
|
||||
// Relabel the certificate in the CMS Keystore
|
||||
err = relabelCertificate(keySet.Name(), &tlsStore.Keystore)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Set key label - for first set of keys only
|
||||
if keyLabel == "" {
|
||||
keyLabel = keySet.Name()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return keyLabel, nil
|
||||
}
|
||||
|
||||
// processTrustCertificates processes all trust certificates - adding them to the CMS KeyStore & PKCS#12 Truststore (if required)
|
||||
func processTrustCertificates(tlsStore *TLSStore, trustDir string) error {
|
||||
|
||||
// Process all trust certiifcates
|
||||
trustList, err := os.ReadDir(trustDir)
|
||||
if err == nil && len(trustList) > 0 {
|
||||
|
||||
// Process each set of keys
|
||||
for _, trustSet := range trustList {
|
||||
keys, _ := os.ReadDir(pathutils.CleanPath(trustDir, trustSet.Name()))
|
||||
|
||||
for _, key := range keys {
|
||||
if strings.HasSuffix(key.Name(), ".crt") {
|
||||
trustSetPath := pathutils.CleanPath(trustDir, trustSet.Name(), key.Name())
|
||||
// #nosec G304 - filename variable is derived from contents of 'trustDir' which is a defined constant
|
||||
file, err := os.ReadFile(trustSetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to read file %s: %v", trustSetPath, err)
|
||||
}
|
||||
|
||||
for string(file) != "" {
|
||||
var block *pem.Block
|
||||
block, file = pem.Decode(file)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Add to known certificates for the CMS Keystore
|
||||
err = addToKnownCertificates(block, &tlsStore.Keystore, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to add to know certificates for CMS Keystore")
|
||||
}
|
||||
|
||||
if tlsStore.Truststore.Keystore != nil {
|
||||
// Add to known certificates for the PKCS#12 Truststore
|
||||
err = addToKnownCertificates(block, &tlsStore.Truststore, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to add to know certificates for PKCS#12 Truststore")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add all trust certificates to PKCS#12 Truststore (if required)
|
||||
if tlsStore.Truststore.Keystore != nil && len(tlsStore.Truststore.TrustedCerts) > 0 {
|
||||
err = addCertificatesToTruststore(&tlsStore.Truststore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Add all trust certificates to CMS Keystore
|
||||
if len(tlsStore.Keystore.TrustedCerts) > 0 {
|
||||
err = addCertificatesToCMSKeystore(&tlsStore.Keystore)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// processPrivateKey processes the private key (*.key) from a set of keys
|
||||
func processPrivateKey(keyDir string, keySetName string, keys []os.DirEntry) (interface{}, string, error) {
|
||||
|
||||
var privateKey interface{}
|
||||
keyPrefix := ""
|
||||
|
||||
for _, key := range keys {
|
||||
|
||||
privateKeyPath := pathutils.CleanPath(keyDir, keySetName, key.Name())
|
||||
if strings.HasSuffix(key.Name(), ".key") {
|
||||
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
|
||||
file, err := os.ReadFile(privateKeyPath)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("Failed to read private key %s: %v", privateKeyPath, err)
|
||||
}
|
||||
block, _ := pem.Decode(file)
|
||||
if block == nil {
|
||||
return nil, "", fmt.Errorf("Failed to decode private key %s: pem.Decode returned nil", privateKeyPath)
|
||||
}
|
||||
|
||||
// Check if the private key is PKCS1
|
||||
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
// Check if the private key is PKCS8
|
||||
privateKey, err = x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("Failed to parse private key %s: %v", privateKeyPath, err)
|
||||
}
|
||||
}
|
||||
keyPrefix = key.Name()[0 : len(key.Name())-len(filepath.Ext(key.Name()))]
|
||||
}
|
||||
}
|
||||
|
||||
return privateKey, keyPrefix, nil
|
||||
}
|
||||
|
||||
// processCertificates processes the certificates (*.crt) from a set of keys
|
||||
func processCertificates(keyDir string, keySetName, keyPrefix string, keys []os.DirEntry, cmsKeystore, p12Truststore *KeyStoreData) (*x509.Certificate, []*x509.Certificate, error) {
|
||||
|
||||
var publicCertificate *x509.Certificate
|
||||
var caCertificate []*x509.Certificate
|
||||
|
||||
for _, key := range keys {
|
||||
|
||||
keystorePath := pathutils.CleanPath(keyDir, keySetName, key.Name())
|
||||
if strings.HasPrefix(key.Name(), keyPrefix) && strings.HasSuffix(key.Name(), ".crt") {
|
||||
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
|
||||
file, err := os.ReadFile(keystorePath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to read public certificate %s: %v", keystorePath, err)
|
||||
}
|
||||
block, file := pem.Decode(file)
|
||||
if block == nil {
|
||||
return nil, nil, fmt.Errorf("Failed to decode public certificate %s: pem.Decode returned nil", keystorePath)
|
||||
}
|
||||
publicCertificate, err = x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to parse public certificate %s: %v", keystorePath, err)
|
||||
}
|
||||
|
||||
// Add to known certificates for the CMS Keystore
|
||||
err = addToKnownCertificates(block, cmsKeystore, false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to add to known certificates for CMS Keystore")
|
||||
}
|
||||
|
||||
// Add to known certificates for the CMS Keystore
|
||||
err = addToKnownCertificates(block, cmsKeystore, false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to add to known certificates for CMS Keystore")
|
||||
}
|
||||
|
||||
// Pick up any other intermediate certificates
|
||||
for string(file) != "" {
|
||||
var block *pem.Block
|
||||
block, file = pem.Decode(file)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Add to known certificates for the CMS Keystore
|
||||
err = addToKnownCertificates(block, cmsKeystore, false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to add to known certificates for CMS Keystore")
|
||||
}
|
||||
|
||||
if p12Truststore.Keystore != nil {
|
||||
// Add to known certificates for the PKCS#12 Truststore
|
||||
err = addToKnownCertificates(block, p12Truststore, true)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to add to known certificates for PKCS#12 Truststore")
|
||||
}
|
||||
}
|
||||
|
||||
certificate, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to parse CA certificate %s: %v", keystorePath, err)
|
||||
}
|
||||
caCertificate = append(caCertificate, certificate)
|
||||
}
|
||||
|
||||
} else if strings.HasSuffix(key.Name(), ".crt") {
|
||||
// #nosec G304 - filename variable is derived from contents of 'keyDir' which is a defined constant
|
||||
file, err := os.ReadFile(keystorePath)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to read CA certificate %s: %v", keystorePath, err)
|
||||
}
|
||||
|
||||
for string(file) != "" {
|
||||
var block *pem.Block
|
||||
block, file = pem.Decode(file)
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// Add to known certificates for the CMS Keystore
|
||||
err = addToKnownCertificates(block, cmsKeystore, false)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to add to known certificates for CMS Keystore")
|
||||
}
|
||||
|
||||
if p12Truststore.Keystore != nil {
|
||||
// Add to known certificates for the PKCS#12 Truststore
|
||||
err = addToKnownCertificates(block, p12Truststore, true)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to add to known certificates for PKCS#12 Truststore")
|
||||
}
|
||||
}
|
||||
|
||||
certificate, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("Failed to parse CA certificate %s: %v", keystorePath, err)
|
||||
}
|
||||
caCertificate = append(caCertificate, certificate)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return publicCertificate, caCertificate, nil
|
||||
}
|
||||
|
||||
// relabelCertificate sets a new label for a certificate in the CMS Keystore
|
||||
func relabelCertificate(newLabel string, cmsKeystore *KeyStoreData) error {
|
||||
|
||||
allLabels, err := cmsKeystore.Keystore.GetCertificateLabels()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to get list of all certificate labels from CMS Keystore: %v", err)
|
||||
}
|
||||
relabelled := false
|
||||
for _, label := range allLabels {
|
||||
found := false
|
||||
for _, keyLabel := range cmsKeystore.KeyLabels {
|
||||
if strings.Trim(label, "\"") == keyLabel {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
err = cmsKeystore.Keystore.RenameCertificate(strings.Trim(label, "\""), newLabel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relabelled = true
|
||||
cmsKeystore.KeyLabels = append(cmsKeystore.KeyLabels, newLabel)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !relabelled {
|
||||
return fmt.Errorf("Failed to relabel certificate for %s in CMS keystore", newLabel)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addCertificatesToTruststore adds trust certificates to the PKCS#12 Truststore
|
||||
func addCertificatesToTruststore(p12Truststore *KeyStoreData) error {
|
||||
|
||||
temporaryPemFile := pathutils.CleanPath("/tmp", "trust.pem")
|
||||
_, err := os.Stat(temporaryPemFile)
|
||||
if err == nil {
|
||||
err = os.Remove(temporaryPemFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to remove file %v: %v", temporaryPemFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = writeCertificatesToFile(temporaryPemFile, p12Truststore.TrustedCerts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = p12Truststore.Keystore.AddNoLabel(temporaryPemFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to add certificates to PKCS#12 Truststore: %v", err)
|
||||
}
|
||||
|
||||
// Relabel all certiifcates
|
||||
allCertificates, err := p12Truststore.Keystore.ListAllCertificates()
|
||||
if err != nil || len(allCertificates) <= 0 {
|
||||
return fmt.Errorf("Failed to get any certificates from PKCS#12 Truststore: %v", err)
|
||||
}
|
||||
|
||||
for i, certificate := range allCertificates {
|
||||
certificate = strings.Trim(certificate, "\"")
|
||||
certificate = strings.TrimSpace(certificate)
|
||||
newLabel := fmt.Sprintf("Trust%d", i)
|
||||
|
||||
err = p12Truststore.Keystore.RenameCertificate(certificate, newLabel)
|
||||
if err != nil || len(allCertificates) <= 0 {
|
||||
return fmt.Errorf("Failed to rename certificate %s to %s in PKCS#12 Truststore: %v", certificate, newLabel, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// addCertificatesToCMSKeystore adds trust certificates to the CMS keystore
|
||||
func addCertificatesToCMSKeystore(cmsKeystore *KeyStoreData) error {
|
||||
|
||||
temporaryPemFile := pathutils.CleanPath("/tmp", "cmsTrust.pem")
|
||||
_, err := os.Stat(temporaryPemFile)
|
||||
if err == nil {
|
||||
err = os.Remove(temporaryPemFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to remove file %v: %v", temporaryPemFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
err = writeCertificatesToFile(temporaryPemFile, cmsKeystore.TrustedCerts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = cmsKeystore.Keystore.AddNoLabel(temporaryPemFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to add certificates to CMS keystore: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateRandomPassword generates a random 12 character password from the characters a-z, A-Z, 0-9
|
||||
func generateRandomPassword() string {
|
||||
pwr.Seed(time.Now().Unix())
|
||||
validChars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
validcharArray := []byte(validChars)
|
||||
password := ""
|
||||
for i := 0; i < 12; i++ {
|
||||
// #nosec G404 - this is only for internal keystore and using math/rand pose no harm.
|
||||
password = password + string(validcharArray[pwr.Intn(len(validcharArray))])
|
||||
}
|
||||
|
||||
return password
|
||||
}
|
||||
|
||||
// addToKnownCertificates adds to the list of known certificates for a Keystore
|
||||
func addToKnownCertificates(block *pem.Block, keyData *KeyStoreData, addToKeystore bool) error {
|
||||
sha512str, err := getCertificateFingerprint(block)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
known := false
|
||||
for _, fingerprint := range keyData.KnownFingerPrints {
|
||||
if fingerprint == sha512str {
|
||||
known = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !known {
|
||||
if addToKeystore {
|
||||
keyData.TrustedCerts = append(keyData.TrustedCerts, block)
|
||||
}
|
||||
keyData.KnownFingerPrints = append(keyData.KnownFingerPrints, sha512str)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getCertificateFingerprint returns a fingerprint for a certificate
|
||||
func getCertificateFingerprint(block *pem.Block) (string, error) {
|
||||
certificate, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Failed to parse x509 certificate: %v", err)
|
||||
}
|
||||
sha512Sum := sha512.Sum512(certificate.Raw)
|
||||
sha512str := string(sha512Sum[:])
|
||||
|
||||
return sha512str, nil
|
||||
}
|
||||
|
||||
// writeCertificatesToFile writes a list of certificates to a file
|
||||
func writeCertificatesToFile(file string, certificates []*pem.Block) error {
|
||||
|
||||
// #nosec G304 - this is a temporary pem file to write certs.
|
||||
f, err := os.Create(file)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to create file %s: %v", file, err)
|
||||
}
|
||||
// #nosec G307 - local to this function, pose no harm.
|
||||
defer f.Close()
|
||||
|
||||
w := bufio.NewWriter(f)
|
||||
|
||||
for i, c := range certificates {
|
||||
err := pem.Encode(w, c)
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to encode certificate number %d: %v", i, err)
|
||||
}
|
||||
err = w.Flush()
|
||||
if err != nil {
|
||||
return fmt.Errorf("Failed to write certificate to file %s: %v", file, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Search the specified directory for .key and .crt files.
|
||||
// Return true if at least one .key or .crt file is found else false
|
||||
func haveKeysAndCerts(keyDir string) bool {
|
||||
fileList, err := os.ReadDir(keyDir)
|
||||
if err == nil && len(fileList) > 0 {
|
||||
for _, fileInfo := range fileList {
|
||||
// Keys and certs will be supplied in an user defined subdirectory.
|
||||
// Do a listing of the subdirectory and then search for .key and .cert files
|
||||
keys, _ := os.ReadDir(pathutils.CleanPath(keyDir, fileInfo.Name()))
|
||||
for _, key := range keys {
|
||||
if strings.HasSuffix(key.Name(), ".key") || strings.HasSuffix(key.Name(), ".crt") {
|
||||
// We found at least one key/crt file.
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Iterate through the certificates to ensure there are no two certificates with same Subject DN.
|
||||
// GSKit does not allow two certificates with same Subject DN/Friendly Names
|
||||
func validateCertificates(personalCert *x509.Certificate, caCertificates []*x509.Certificate) error {
|
||||
// Check if we have been asked to override certificate validation by setting
|
||||
// MQ_ENABLE_CERT_VALIDATION to false
|
||||
enableValidation, enableValidationSet := os.LookupEnv("MQ_ENABLE_CERT_VALIDATION")
|
||||
if !enableValidationSet || (enableValidationSet && !strings.EqualFold(strings.Trim(enableValidation, ""), "false")) {
|
||||
for _, caCert := range caCertificates {
|
||||
if strings.EqualFold(personalCert.Subject.String(), caCert.Subject.String()) {
|
||||
return fmt.Errorf("Error: The Subject DN of the Issuer Certificate and the Queue Manager are same")
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
99
internal/tls/tls_web.go
Normal file
99
internal/tls/tls_web.go
Normal file
@@ -0,0 +1,99 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2019, 2023
|
||||
|
||||
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 tls
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/ibm-messaging/mq-container/internal/keystore"
|
||||
"github.com/ibm-messaging/mq-container/internal/mqtemplate"
|
||||
"github.com/ibm-messaging/mq-container/internal/pathutils"
|
||||
"github.com/ibm-messaging/mq-container/internal/securityutility"
|
||||
"github.com/ibm-messaging/mq-container/pkg/logger"
|
||||
)
|
||||
|
||||
// webKeystoreDefault is the name of the default web server Keystore
|
||||
const webKeystoreDefault = "default.p12"
|
||||
|
||||
// ConfigureWebTLS configures TLS for the web server
|
||||
func ConfigureWebTLS(keyLabel string, log *logger.Logger, password string) error {
|
||||
|
||||
// Return immediately if we have no certificate to use as identity
|
||||
if keyLabel == "" && os.Getenv("MQ_GENERATE_CERTIFICATE_HOSTNAME") == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
tlsConfigLink := "/run/tls.xml"
|
||||
tlsConfigTemplate := "/etc/mqm/web/installations/Installation1/servers/mqweb/tls.xml.tpl"
|
||||
encryptedPassword, err := securityutility.EncodeSecrets(password)
|
||||
if err != nil {
|
||||
log.Printf("Password encoding for Web Keystore failed with error %v", err)
|
||||
// We couldn't encode the passwords so using an empty string as password
|
||||
encryptedPassword = ""
|
||||
}
|
||||
// Password successfully encoded using securityUtility use the encoded password the template
|
||||
templateErr := mqtemplate.ProcessTemplateFile(tlsConfigTemplate, tlsConfigLink, map[string]string{"password": encryptedPassword}, log)
|
||||
if templateErr != nil {
|
||||
return templateErr
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigureWebKeyStore configures the Web Keystore
|
||||
func ConfigureWebKeystore(p12Truststore KeyStoreData, keyLabel string) (string, error) {
|
||||
|
||||
webKeystore := webKeystoreDefault
|
||||
if keyLabel != "" {
|
||||
webKeystore = keyLabel + ".p12"
|
||||
}
|
||||
webKeystoreFile := pathutils.CleanPath(keystoreDirDefault, webKeystore)
|
||||
|
||||
// Check if a new self-signed certificate should be generated
|
||||
if keyLabel == "" {
|
||||
|
||||
// Get hostname to use for self-signed certificate
|
||||
genHostName := os.Getenv("MQ_GENERATE_CERTIFICATE_HOSTNAME")
|
||||
|
||||
// Create the Web Keystore
|
||||
newWebKeystore := keystore.NewPKCS12KeyStore(webKeystoreFile, p12Truststore.Password)
|
||||
err := newWebKeystore.Create()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create Web Keystore %s: %v", webKeystoreFile, err)
|
||||
}
|
||||
|
||||
// Generate a new self-signed certificate in the Web Keystore
|
||||
err = newWebKeystore.CreateSelfSignedCertificate("default", fmt.Sprintf("CN=%s", genHostName), genHostName)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate certificate in Web Keystore %s with DN of 'CN=%s': %v", webKeystoreFile, genHostName, err)
|
||||
}
|
||||
} else {
|
||||
// Check Web Keystore already exists
|
||||
_, err := os.Stat(webKeystoreFile)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find existing Web Keystore %s: %v", webKeystoreFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Check Web Truststore already exists
|
||||
_, err := os.Stat(p12Truststore.Keystore.Filename)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to find existing Web Truststore %s: %v", p12Truststore.Keystore.Filename, err)
|
||||
}
|
||||
|
||||
return webKeystore, nil
|
||||
}
|
||||
41
internal/user/user.go
Normal file
41
internal/user/user.go
Normal file
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
© Copyright IBM Corporation 2018, 2020
|
||||
|
||||
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 user
|
||||
|
||||
import (
|
||||
"golang.org/x/sys/unix"
|
||||
)
|
||||
|
||||
// User holds information on primary and supplemental OS groups
|
||||
type User struct {
|
||||
UID int
|
||||
PrimaryGID int
|
||||
SupplementalGID []int
|
||||
}
|
||||
|
||||
// GetUser returns the current user and group information
|
||||
func GetUser() (User, error) {
|
||||
u := User{
|
||||
UID: unix.Geteuid(),
|
||||
PrimaryGID: unix.Getgid(),
|
||||
}
|
||||
groups, err := unix.Getgroups()
|
||||
if err != nil {
|
||||
return u, err
|
||||
}
|
||||
u.SupplementalGID = groups
|
||||
return u, nil
|
||||
}
|
||||
Reference in New Issue
Block a user