From 79f0bcf155b35a2a4555b35bfe7b26101cecd347 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Tue, 4 Jun 2024 17:18:06 -0700 Subject: [PATCH 01/12] MLE-11460 Single Group Creation --- go.mod | 4 +- .../controller/marklogicgroup_controller.go | 1 - pkg/k8sutil/configmap.go | 916 ++++++++++++++++++ pkg/k8sutil/handler.go | 5 + pkg/k8sutil/statefulset.go | 61 +- 5 files changed, 974 insertions(+), 13 deletions(-) create mode 100644 pkg/k8sutil/configmap.go diff --git a/go.mod b/go.mod index f47f35b..68aaf1e 100644 --- a/go.mod +++ b/go.mod @@ -3,8 +3,10 @@ module github.com/marklogic/marklogic-kubernetes-operator go 1.20 require ( + github.com/go-logr/logr v1.2.4 github.com/onsi/ginkgo/v2 v2.11.0 github.com/onsi/gomega v1.27.10 + k8s.io/api v0.28.3 k8s.io/apimachinery v0.28.3 k8s.io/client-go v0.28.3 sigs.k8s.io/controller-runtime v0.16.3 @@ -17,7 +19,6 @@ require ( github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/zapr v1.2.4 // indirect github.com/go-openapi/jsonpointer v0.19.6 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect @@ -61,7 +62,6 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/api v0.28.3 // indirect k8s.io/apiextensions-apiserver v0.28.3 // indirect k8s.io/component-base v0.28.3 // indirect k8s.io/klog/v2 v2.100.1 // indirect diff --git a/internal/controller/marklogicgroup_controller.go b/internal/controller/marklogicgroup_controller.go index d7ed117..7a048c5 100644 --- a/internal/controller/marklogicgroup_controller.go +++ b/internal/controller/marklogicgroup_controller.go @@ -70,7 +70,6 @@ func (r *MarklogicGroupReconciler) Reconcile(ctx context.Context, req ctrl.Reque log.IntoContext(ctx, logger) - // logger.Info("Operator Status: ", "Conditions", &operatorCR.Status.Conditions) oc, err := k8sutil.CreateOperatorContext(ctx, &req, r.Client, r.Scheme, r.Recorder) logger.Info("==== Reconciling MarklogicGroup") diff --git a/pkg/k8sutil/configmap.go b/pkg/k8sutil/configmap.go new file mode 100644 index 0000000..13afc93 --- /dev/null +++ b/pkg/k8sutil/configmap.go @@ -0,0 +1,916 @@ +package k8sutil + +import ( + "github.com/marklogic/marklogic-kubernetes-operator/pkg/result" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func (oc *OperatorContext) ReconcileConfigMap() result.ReconcileResult { + logger := oc.ReqLogger + client := oc.Client + cr := oc.MarklogicGroup + + logger.Info("Reconciling MarkLogic ConfigMap") + labels := getMarkLogicLabels(cr.Spec.Name) + annotations := map[string]string{} + configMapName := cr.Spec.Name + "-scripts" + objectMeta := generateObjectMeta(configMapName, cr.Namespace, labels, annotations) + namespace := objectMeta.Namespace + nsName := types.NamespacedName{Name: objectMeta.Name, Namespace: objectMeta.Namespace} + configmap := &corev1.ConfigMap{} + err := client.Get(oc.Ctx, nsName, configmap) + if err != nil { + if errors.IsNotFound(err) { + logger.Info("MarkLogic sripts ConfigMap is not found, creating a new one") + configmapDef := generateConfigMapDef(objectMeta, marklogicServerAsOwner(cr)) + err = oc.createConfigMap(namespace, configmapDef) + if err != nil { + logger.Info("MarkLogic scripts configmap creation is failed") + return result.Error(err) + } + logger.Info("MarkLogic scripts configmap creation is successful") + // result.Continue() + } else { + logger.Error(err, "MarkLogic scripts configmap creation is failed") + return result.Error(err) + } + } + + return result.Continue() +} + +func generateConfigMapDef(serviceMeta metav1.ObjectMeta, ownerRef metav1.OwnerReference) *corev1.ConfigMap { + configmap := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + Kind: "ConfigMap", + APIVersion: "v1", + }, + ObjectMeta: serviceMeta, + Data: map[string]string{ + "liveness-probe.sh": getLivenessProbeScript(), + "copy-certs.sh": getCopyCertScript(), + "prestop-hook.sh": getPrestopHookScript(), + "poststart-hook.sh": getPoststartHookScript(), + }, + } + configmap.SetOwnerReferences(append(configmap.GetOwnerReferences(), ownerRef)) + return configmap +} + +func (oc *OperatorContext) createConfigMap(namespace string, configMap *corev1.ConfigMap) error { + logger := oc.ReqLogger + client := oc.Client + err := client.Create(oc.Ctx, configMap) + if err != nil { + logger.Error(err, "MarkLogic script configmap creation is failed") + return err + } + logger.Info("MarkLogic script configmap creation is successful") + return nil +} + +func getLivenessProbeScript() string { + return `#!/bin/bash +log () { + local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") + # Check to make sure pod doesn't terminate if PID value is empty for any reason + if [ -n "$pid" ]; then + echo "${TIMESTAMP} $@" > /proc/$pid/fd/1 + fi +} + +pid=$(pgrep -fn start.marklogic) + +# Check if ML service is running. Exit with 1 if it is other than running +ml_status=$(/etc/init.d/MarkLogic status) + +if [[ "$ml_status" =~ "running" ]]; then + http_code=$(curl -o /tmp/probe_response.txt -s -w "%{http_code}" "http://${HOSTNAME}:8001/admin/v1/timestamp") + curl_code=$? + http_resp=$(cat /tmp/probe_response.txt) + + if [[ $curl_code -ne 0 && $http_code -ne 401 ]]; then + log "Info: [Liveness Probe] Error with MarkLogic" + log "Info: [Liveness Probe] Curl response code: "$curl_code + log "Info: [Liveness Probe] Http response code: "$http_code + log "Info: [Liveness Probe] Http response message: "$http_resp + fi + rm -f /tmp/probe_response.txt + exit 0 +else + exit 1 +fi +` +} + +func getCopyCertScript() string { + return `#!/bin/bash +MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" +MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/username)" +log () { + local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") + echo "${TIMESTAMP} $@" +} +if [[ -d "/tmp/server-cert-secrets" ]]; then + certType="named" +else + certType="self-signed" +fi +log "Info: [copy-certs] Proceeding with $certType certificate flow." +host_FQDN="$POD_NAME.$MARKLOGIC_FQDN_SUFFIX" +log "Info: [copy-certs] FQDN for this server: $host_FQDN" +foundMatchingCert="false" +if [[ "$certType" == "named" ]]; then + cp -f /tmp/ca-cert-secret/* /run/secrets/marklogic-certs/; + cert_paths=$(find /tmp/server-cert-secrets/tls_*.crt) + for cert_path in $cert_paths; do + cert_cn=$(openssl x509 -noout -subject -in $cert_path | sed -n 's/.*CN = \([^,]*\).*/\1/p') + log "Info: [copy-certs] FQDN for the certificate: $cert_cn" + if [[ "$host_FQDN" == "$cert_cn" ]]; then + log "Info: [copy-certs] found certificate for the server" + foundMatchingCert="true" + cp $cert_path /run/secrets/marklogic-certs/tls.crt + pkey_path=$(echo "$cert_path" | sed "s:.crt:.key:") + cp $pkey_path /run/secrets/marklogic-certs/tls.key + if [[ ! -e "$pkey_path" ]]; then + log "Error: [copy-certs] private key tls.key for certificate $cert_cn is not found. Exiting." + exit 1 + fi + + # verify the tls.crt and cacert.pem is valid, otherwise exit + openssl verify -CAfile /run/secrets/marklogic-certs/cacert.pem /run/secrets/marklogic-certs/tls.crt + if [[ $? -ne 0 ]]; then + log "Error: [copy-certs] Server certificate tls.crt verification with cacert.pem failed. Exiting." + exit 1 + fi + # verify the tls.crt and tls.key is matching, otherwise exit + privateKeyMD5=$(openssl rsa -modulus -noout -in /run/secrets/marklogic-certs/tls.key | openssl md5) + publicKeyMD5=$(openssl x509 -modulus -noout -in /run/secrets/marklogic-certs/tls.crt | openssl md5) + if [[ -z "privateKeyMD5" ]] || [[ "$privateKeyMD5" != "$publicKeyMD5" ]]; then + log "Error: [copy-certs] private key tls.key and server certificate tls.crt are not matching. Exiting." + exit 1 + fi + log "Info: [copy-certs] certificate and private key are valid." + break + fi + done + if [[ $foundMatchingCert == "false" ]]; then + if [[ $POD_NAME = *"-0" ]]; then + log "Error: [copy-certs] Failed to find matching certificate for the bootstrap server. Exiting." + exit 1 + else + log "Error: [copy-certs] Failed to find matching certificate for the non-bootstrap server. Continuing with temporary certificate for this host. Please update the certificate for this host later." + fi + fi +elif [[ "$certType" == "self-signed" ]]; then + if [[ $POD_NAME != *"-0" ]] || [[ $MARKLOGIC_CLUSTER_TYPE == "non-bootstrap" ]]; then + log "Info: [copy-certs] Getting CA for bootstrap host" + cd /run/secrets/marklogic-certs/ + echo quit | openssl s_client -showcerts -servername "${MARKLOGIC_BOOTSTRAP_HOST}" -showcerts -connect "${MARKLOGIC_BOOTSTRAP_HOST}":8000 2>&1 < /dev/null | sed -n '/-----BEGIN/,/-----END/p' > cacert.pem + fi +else + log "Error: [copy-certs] unknown certType: $certType" + exit 1 +fi +` +} + +func getPrestopHookScript() string { + return `#!/bin/bash +MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" +MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/password)" + +log () { + local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") + # Check to make sure pod doesn't terminate if PID value is empty for any reason + # If PID value is empty preStart hook logs are not recorded + if [ -n "$pid" ]; then + echo "${TIMESTAMP} $@" > /proc/$pid/fd/1 + fi +} + +pid=$(pgrep -fn start.marklogic) +log "Info: [prestop] Prestop Hook Execution" + +my_host=$(hostname -f) + +HTTP_PROTOCOL="http" +HTTPS_OPTION="" +if [[ "$MARKLOGIC_JOIN_TLS_ENABLED" == "true" ]]; then + HTTP_PROTOCOL="https" + HTTPS_OPTION="-k" +fi +log "Info: [prestop] MarkLogic Pod Hostname: "$my_host +for ((i = 0; i < 5; i = i + 1)); do + res_code=$(curl --anyauth --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD \ + -o /dev/null -m 10 -s -w %{http_code} \ + -i -X POST ${HTTPS_OPTION} --data "state=shutdown&failover=true" \ + -H "Content-type: application/x-www-form-urlencoded" \ + ${HTTP_PROTOCOL}://localhost:8002/manage/v2/hosts/$my_host?format=json) + + if [[ ${res_code} -eq 202 ]]; then + log "Info: [prestop] Host shut down response code: "$res_code + + while (true) + do + ml_status=$(service MarkLogic status) + log "Info: [prestop] MarkLogic Status: "$ml_status + if [[ "$ml_status" =~ "running" ]]; then + sleep 5s + continue + else + break + fi + done + break + else + log "ERROR: [prestop] Retry Attempt: "$i + log "ERROR: [prestop] Host shut down expected response code 202, got "$res_code + sleep 10s + fi +done +` +} + +func getPoststartHookScript() string { + return `#!/bin/bash +# Refer to https://docs.marklogic.com/guide/admin-api/cluster#id_10889 for cluster joining process + +N_RETRY=60 +RETRY_INTERVAL=1 +HOST_FQDN="$(hostname).${MARKLOGIC_FQDN_SUFFIX}" + +# HTTP_PROTOCOL could be http or https +HTTP_PROTOCOL="http" +HTTPS_OPTION="" +if [[ "$MARKLOGIC_JOIN_TLS_ENABLED" == "true" ]]; then + HTTP_PROTOCOL="https" + HTTPS_OPTION="-k" +fi + +IS_BOOTSTRAP_HOST=false +if [[ "$(hostname)" == *-0 ]]; then + echo "IS_BOOTSTRAP_HOST true" + IS_BOOTSTRAP_HOST=true +else + echo "IS_BOOTSTRAP_HOST false" +fi + +# MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" +# MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/password)" +MARKLOGIC_ADMIN_USERNAME="admin" +MARKLOGIC_ADMIN_PASSWORD="admin" + +pid=$(pgrep -fn start.marklogic) + +############################################################### +# Logging utility +############################################################### +info() { + log "Info" "$@" +} + +error() { + log "Error" "$1" + local EXIT_STATUS="$2" + if [[ ${EXIT_STATUS} == "exit" ]] + then + exit 1 + fi +} + +log () { + local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") + # Check to make sure pod doesn't terminate if PID value is empty for any reason + # If PID value is empty postStart hook logs are not recorded + message="${TIMESTAMP} [postStart] $@" + if [ -n "$pid" ]; then + echo $message > /proc/$pid/fd/1 + fi + + echo $message >> /tmp/script.log +} + +################################################################ +# restart_check(hostname, baseline_timestamp) +# +# Use the timestamp service to detect a server restart, given a +# a baseline timestamp. Use N_RETRY and RETRY_INTERVAL to tune +# the test length. Include authentication in the curl command +# so the function works whether or not security is initialized. +# $1 : The hostname to test against +# $2 : The baseline timestamp +# Returns 0 if restart is detected, exits with an error if not. +################################################################ +function restart_check { + local hostname=$1 + local old_timestamp=$2 + local retry_count + local last_start + + info "${hostname} - waiting for MarkLogic to restart" + + last_start=$( \ + curl -s --anyauth \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + "http://${hostname}:8001/admin/v1/timestamp" \ + ) + for ((retry_count = 0; retry_count < N_RETRY; retry_count = retry_count + 1)); do + if [ "${old_timestamp}" == "${last_start}" ] || [ -z "${last_start}" ]; then + info "${hostname} - waiting for MarkLogic to restart: ${old_timestamp} ${last_start}" + sleep ${RETRY_INTERVAL} + last_start=$( \ + curl -s --anyauth \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + "http://${hostname}:8001/admin/v1/timestamp" \ + ) + else + info "${hostname} - MarkLogic has restarted" + return 0 + fi + done + error "${hostname} - failed to restart" exit +} +################################################################ +# retry_and_timeout(target_url, expected_response_code, additional_options, return_error) +# The third argument is optional and can be used to pass additional options to curl. +# Fourth argurment is optional, default is set to true, can be used when custom error handling is required, +# if set to true means function will return error and exit if curl fails N_RETRY times +# setting to false means function will return response code instead of failing and exiting. +# Retry a curl command until it returns the expected response +# code or fails N_RETRY times. +# Use RETRY_INTERVAL to tune the test length. +# Validate that response code is the same as expected response +# code or exit with an error. +# +# $1 : The target url to test against +# $2 : The expected response code +# $3 : Additional options to pass to curl +# $4 : Option to return error or response code in case of error +################################################################ +function curl_retry_validate { + local retry_count + local return_error="${4:-true}" + for ((retry_count = 0; retry_count < N_RETRY; retry_count = retry_count + 1)); do + request="curl -m 30 -s -w '%{http_code}' $3 $1" + response_code=$(eval "${request}") + if [[ ${response_code} -eq $2 ]]; then + return "${response_code}" + fi + sleep ${RETRY_INTERVAL} + done + if [[ "${return_error}" = "false" ]] ; then + return "${response_code}" + fi + error "Expected response code ${2}, got ${response_code} from ${1}." exit +} + +################################################################ +# Function to initialize a host +# $1: The host name +# return values: 0 - successfully initialized +# 1 - host not reachable +################################################################ +function wait_until_marklogic_ready { + local host=$1 + info "wait until $host is ready" + timestamp=$( curl -s --anyauth \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + http://${host}:8001/admin/v1/timestamp ) + if [ -z "${timestamp}" ]; then + info "${host} - not responding yet" + sleep 5s + wait_until_marklogic_ready $host + else + info "${host} - responding, calling init" + out="/tmp/${host}.out" + + response_code=$( \ + curl --anyauth -m 30 -s --retry 5 \ + -w '%{http_code}' -o "${out}" \ + -i -X POST -H "Content-type:application/json" \ + -d "${LICENSE_PAYLOAD}" \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + http://${host}:8001/admin/v1/init \ + ) + if [ "${response_code}" = "202" ]; then + info "${host} - init called, restart triggered" + last_startup=$( \ + cat "${out}" | + grep "last-startup" | + sed 's%^.*\(.*\).*$%\1%' \ + ) + + restart_check "${host}" "${last_startup}" + info "${host} - restarted" + info "${host} - init complete" + elif [ "${response_code}" -eq "204" ]; then + info "${host} - init called, no restart triggered" + info "${host} - init complete" + else + info "${host} - error calling init: ${response_code}" + fi + fi +} + +################################################################ +# Function to initialize a host +# $1: The host name +# return values: 0 - successfully initialized +# 1 - host not reachable +################################################################ +function init_marklogic_host { + local hostname=$1 + info "initializing host: $hostname" + timestamp=$( \ + curl -s --anyauth \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + http://${hostname}:8001/admin/v1/timestamp \ + ) + if [ -z "${timestamp}" ]; then + info "${hostname} - not responding yet" + return 1 + fi + info "${hostname} - responding, calling init" + output_path="/tmp/${hostname}.out" + response_code=$( \ + curl --anyauth -m 30 -s --retry 5 \ + -w '%{http_code}' -o "${output_path}" \ + -i -X POST -H "Content-type:application/json" \ + -d "${LICENSE_PAYLOAD}" \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + http://${hostname}:8001/admin/v1/init \ + ) + + if [ "${response_code}" = "202" ]; then + info "${hostname} - init called, restart triggered" + last_startup=$( \ + cat "${output_path}" | + grep "last-startup" | + sed 's%^.*\(.*\).*$%\1%' \ + ) + + restart_check "${hostname}" "${last_startup}" + return 0 + elif [ "${response_code}" -eq "204" ]; then + info "${hostname} - init called, no restart triggered" + info "${hostname} - init complete" + return 0 + else + info "${hostname} - error calling init: ${response_code}" + [ -f "${out}" ] && cat "${out}" + fi +} + +################################################################ +# Function to bootstrap host is ready: +# 1. If TLS is not enabled, wait until Security DB is installed. +# 2. If TLS is enabled, wait until TLS is turned on in App Server +# return values: 0 - admin user successfully initialized +################################################################ +function wait_bootstrap_ready { + resp=$(curl -w '%{http_code}' -o /dev/null http://$MARKLOGIC_BOOTSTRAP_HOST:8001/admin/v1/timestamp ) + if [[ "$MARKLOGIC_JOIN_TLS_ENABLED" == "true" ]]; then + # return 403 if tls is enabled + if [[ $resp -eq 403 ]]; then + info "Bootstrap host is ready with TLS enabled" + else + info "Timestamp response code:$resp. Bootstrap host is not ready with TLS enabled, try again in 10s" + sleep 10s + wait_bootstrap_ready + fi + else + if [[ $resp -eq 401 ]]; then + info "Bootstrap host is ready with no TLS" + else + info "Timestamp response code:$resp. Bootstrap host is not ready, try again in 10s" + sleep 10s + wait_bootstrap_ready + fi + fi +} + +################################################################ +# Function to initialize admin user and security DB +# +# return values: 0 - admin user successfully initialized +################################################################ +function init_security_db { + info "initializing as bootstrap cluster" + + # check to see if the bootstrap host is already configured + response_code=$( \ + curl -s --anyauth \ + -w '%{http_code}' -o "/tmp/${MARKLOGIC_BOOTSTRAP_HOST}.out" \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" $HTTPS_OPTION \ + $HTTP_PROTOCOL://$MARKLOGIC_BOOTSTRAP_HOST:8002/manage/v2/hosts/$MARKLOGIC_BOOTSTRAP_HOST/properties + ) + + if [ "${response_code}" = "200" ]; then + info "${MARKLOGIC_BOOTSTRAP_HOST} - bootstrap security already initialized" + return 0 + else + info "${MARKLOGIC_BOOTSTRAP_HOST} - initializing bootstrap security" + + # Get last restart timestamp directly before instance-admin call to verify restart after + timestamp=$( \ + curl -s --anyauth \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + "http://${MARKLOGIC_BOOTSTRAP_HOST}:8001/admin/v1/timestamp" \ + ) + + curl_retry_validate "http://${MARKLOGIC_BOOTSTRAP_HOST}:8001/admin/v1/instance-admin" 202 \ + "-o /dev/null \ + -X POST -H \"Content-type:application/x-www-form-urlencoded; charset=utf-8\" \ + -d \"admin-username=${MARKLOGIC_ADMIN_USERNAME}\" --data-urlencode \"admin-password=${MARKLOGIC_ADMIN_PASSWORD}\" \ + -d \"realm=${ML_REALM}\" -d \"${MARKLOGIC_WALLET_PASSWORD_PAYLOAD}\"" + + restart_check "${MARKLOGIC_BOOTSTRAP_HOST}" "${timestamp}" + + info "${MARKLOGIC_BOOTSTRAP_HOST} - bootstrap security initialized" + return 0 + fi +} + +################################################################ +# Function to join marklogic host to cluster +# +# return values: 0 - admin user successfully initialized +################################################################ +function join_cluster { + hostname=$1 + + # check if Bootstrap Host is ready + # if server could not be reached, response_code == 000 + # if host has not join cluster, return 404 + # if bootstrap host not init, return 403 + # if Security DB not set or credential not correct return 401 + # if host is already in cluster, return 200 + response_code=$( curl -s --anyauth -o /dev/null -w '%{http_code}' \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" $HTTPS_OPTION \ + $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/hosts/${hostname}/properties?format=xml \ + ) + + info "response_code: $response_code" + + if [ "${response_code}" = "200" ]; then + info "host has already joined the cluster" + return 0 + elif [ "${response_code}" != "404" ]; then + sleep 10s + join_cluster $hostname + else + info "Proceed to joining bootstrap host" + fi + + # process to join the host + + # Wait until the group is ready + retry_count=10 + while [ $retry_count -gt 0 ]; do + GROUP_RESP_CODE=$( curl --anyauth -m 20 -s -o /dev/null -w "%{http_code}" $HTTPS_OPTION -X GET $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups/${MARKLOGIC_GROUP} --anyauth --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} ) + if [[ ${GROUP_RESP_CODE} -eq 200 ]]; then + info "Found the group, process to join the group" + break + else + ((retry_count--)) + info "GROUP_RESP_CODE: $GROUP_RESP_CODE , retry $retry_count times to joining ${MARKLOGIC_GROUP} group in marklogic cluster" + sleep 10s + fi + done + + if [[ $retry_count -le 0 ]]; then + info "retry_count: $retry_count" + error "pass timeout to wait for the group ready" + exit 1 + fi + + info "${hostname} - joining group ${MARKLOGIC_GROUP}" + payload=\"group=${MARKLOGIC_GROUP}\" + curl_retry_validate "http://${hostname}:8001/admin/v1/server-config" 200 \ + "--anyauth --user \"${MARKLOGIC_ADMIN_USERNAME}\":\"${MARKLOGIC_ADMIN_PASSWORD}\" \ + -o /tmp/${hostname}.xml -X GET -H \"Accept: application/xml\"" + + curl_retry_validate "$HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8001/admin/v1/cluster-config" 200 \ + "--anyauth $HTTPS_OPTION --user \"${MARKLOGIC_ADMIN_USERNAME}\":\"${MARKLOGIC_ADMIN_PASSWORD}\" \ + -X POST -d \"${payload}\" \ + --data-urlencode \"server-config@/tmp/${hostname}.xml\" \ + -H \"Content-type: application/x-www-form-urlencoded\" \ + -o /tmp/${hostname}_cluster.zip" + + timestamp=$( \ + curl -s --anyauth $HTTPS_OPTION \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + "http://${hostname}:8001/admin/v1/timestamp" \ + ) + + curl_retry_validate "http://${hostname}:8001/admin/v1/cluster-config" 202 \ + "-o /dev/null --anyauth --user \"${MARKLOGIC_ADMIN_USERNAME}\":\"${MARKLOGIC_ADMIN_PASSWORD}\" \ + -X POST -H \"Content-type: application/zip\" \ + --data-binary @/tmp/${hostname}_cluster.zip" + + # 202 causes restart + info "${hostname} - restart triggered" + # restart_check "${hostname}" "${timestamp}" + + info "${hostname} - joined group ${MARKLOGIC_GROUP}" +} + +################################################################ +# Function to configure MarkLogic Group +# +# return +################################################################ +function configure_group { + + if [[ "$IS_BOOTSTRAP_HOST" == "true" ]]; then + group_cfg_template='{"group-name":"%s", "xdqp-ssl-enabled":"%s"}' + group_cfg=$(printf "$group_cfg_template" "$MARKLOGIC_GROUP" "$XDQP_SSL_ENABLED") + + # check if host is already in and get the current cluster + response_code=$( \ + curl -s --anyauth \ + -w '%{http_code}' -o "/tmp/groups.out" \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + http://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/hosts/${HOST_FQDN}/properties?format=xml + ) + + if [ "${response_code}" = "200" ]; then + current_group=$( \ + cat "/tmp/groups.out" | + grep "group" | + sed 's%^.*\(.*\).*$%\1%' \ + ) + + info "current_group: $current_group" + info "group_cfg: $group_cfg" + + # curl retry doesn't work in the lower version + response_code=$( \ + curl -s --anyauth \ + --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} \ + -w '%{http_code}' \ + -X PUT \ + -H "Content-type: application/json" \ + -d "${group_cfg}" \ + http://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups/${current_group}/properties \ + ) + + info "response_code: $response_code" + + if [[ "${response_code}" = "204" ]]; then + info "group \"${current_group}\" updated" + elif [[ "${response_code}" = "202" ]]; then + # Note: THIS SHOULD NOT HAPPEN WITH THE CURRENT GROUP CONFIG + info "group \"${current_group}\" updated and a restart of all hosts in the group was triggered" + else + info "unexpected response when updating group \"${current_group}\": ${response_code}" + fi + + fi + + if [[ "$MARKLOGIC_CLUSTER_TYPE" == "non-bootstrap" ]]; then + info "creating group for other Helm Chart" + + # Create a group if group is not already exits + GROUP_RESP_CODE=$( curl --anyauth -m 20 -s -o /dev/null -w "%{http_code}" $HTTPS_OPTION -X GET $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups/${MARKLOGIC_GROUP} --anyauth --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} ) + if [[ ${GROUP_RESP_CODE} -eq 200 ]]; then + info "Skipping creation of group $MARKLOGIC_GROUP as it already exists on the MarkLogic cluster." + else + res_code=$(curl --anyauth --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} $HTTPS_OPTION -m 20 -s -w '%{http_code}' -X POST -d "${group_cfg}" -H "Content-type: application/json" $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups) + if [[ ${res_code} -eq 201 ]]; then + log "Info: [initContainer] Successfully configured group $MARKLOGIC_GROUP on the MarkLogic cluster." + else + log "Info: [initContainer] Expected response code 201, got $res_code" + fi + fi + + fi + else + info "not bootstrap host. Skip group configuration" + fi + +} + +function configure_tls { + info "Configuring TLS for App Servers" + + AUTH_CURL="curl --anyauth --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s " + + cd /tmp/ + if [[ -e "/run/secrets/marklogic-certs/tls.crt" ]]; then + info "Configuring named certificates on host" + certType="named" + else + info "Configuring self-signed certificates on host" + certType="self-signed" + fi + info "certType in postStart: $certType" + + cat <<'EOF' > defaultCertificateTemplate.json +{ + "template-name": "defaultTemplate", + "template-description": "defaultTemplate", + "key-type": "rsa", + "key-options": { + "key-length": "2048" + }, + "req": { + "version": "0", + "subject": { + "organizationName": "MarkLogic" + } + } +} +EOF + +if [[ $POD_NAME == *-0 ]] && [[ $MARKLOGIC_CLUSTER_TYPE == "bootstrap" ]]; then + log "Info: creating default certificate Template" + response=$($AUTH_CURL -X POST --header "Content-Type:application/json" -d @defaultCertificateTemplate.json http://localhost:8002/manage/v2/certificate-templates) + sleep 5s + log "Info: done creating default certificate Template" + fi + + log "Info: creating insert-host-certificates.json" + cat <<'EOF' > insert-host-certificates.json + { + "operation": "insert-host-certificates", + "certificates": [ + { + "certificate": { + "cert": "CERT", + "pkey": "PKEY" + } + } + ] + } +EOF + + log "Info: creating generateCA.xqy" + cat <<'EOF' > generateCA.xqy +xquery= + xquery version "1.0-ml"; + import module namespace pki = "http://marklogic.com/xdmp/pki" + at "/MarkLogic/pki.xqy"; + let $tid := pki:template-get-id(pki:get-template-by-name("defaultTemplate")) + return + pki:generate-template-certificate-authority($tid, 365) +EOF + + log "Info: creating createTempCert.xqy" + cat <<'EOF' > createTempCert.xqy +xquery= + xquery version "1.0-ml"; + import module namespace pki = "http://marklogic.com/xdmp/pki" + at "/MarkLogic/pki.xqy"; + import module namespace admin = "http://marklogic.com/xdmp/admin" + at "/MarkLogic/admin.xqy"; + let $tid := pki:template-get-id(pki:get-template-by-name("defaultTemplate")) + let $config := admin:get-configuration() + let $hostname := admin:host-get-name($config, admin:host-get-id($config, xdmp:host-name())) + return + pki:generate-temporary-certificate-if-necessary($tid, 365, $hostname, (), ()) +EOF + + log "Info: inserting certificates $certType" + if [[ "$certType" == "named" ]]; then + log "Info: creating named certificate" + cert_path="/run/secrets/marklogic-certs/tls.crt" + pkey_path="/run/secrets/marklogic-certs/tls.key" + cp insert-host-certificates.json insert_cert_payload.json + cert="$(<$cert_path)" + cert="${cert//$'\n'/}" + pkey="$(<$pkey_path)" + pkey="${pkey//$'\n'/}" + + sed -i "s|CERT|$cert|" insert_cert_payload.json + sed -i "s|CERTIFICATE-----|CERTIFICATE-----\\\\n|" insert_cert_payload.json + sed -i "s|-----END CERTIFICATE|\\\\n-----END CERTIFICATE|" insert_cert_payload.json + sed -i "s|PKEY|$pkey|" insert_cert_payload.json + sed -i "s|PRIVATE KEY-----|PRIVATE KEY-----\\\\n|" insert_cert_payload.json + sed -i "s|-----END RSA|\\\\n-----END RSA|" insert_cert_payload.json + sed -i "s|-----END PRIVATE|\\\\n-----END PRIVATE|" insert_cert_payload.json + + log "Info: inserting following certificates for $cert_path for $MARKLOGIC_CLUSTER_TYPE" + + if [[ $POD_NAME == *-0 ]]; then + res=$($AUTH_CURL -X POST --header "Content-Type:application/json" -d @insert_cert_payload.json http://localhost:8002/manage/v2/certificate-templates/defaultTemplate 2>&1) + else + res=$($AUTH_CURL -k -X POST --header "Content-Type:application/json" -d @insert_cert_payload.json https://localhost:8002/manage/v2/certificate-templates/defaultTemplate 2>&1) + fi + log "Info: $res" + sleep 5s + fi + + if [[ $POD_NAME == *-0 ]]; then + if [[ $MARKLOGIC_CLUSTER_TYPE == "bootstrap" ]]; then + log "Info: Generating Temporary CA Certificate" + $AUTH_CURL -X POST -i -d @generateCA.xqy \ + -H "Content-type: application/x-www-form-urlencoded" \ + -H "Accept: multipart/mixed; boundary=BOUNDARY" \ + http://localhost:8000/v1/eval + resp_code=$? + info "response code for Generating Temporary CA Certificate is $resp_code" + sleep 5s + fi + + log "Info: enabling app-servers for HTTPS" + # Manage need be put in the last in the array to make sure http works for all the requests + appServers=("App-Services" "Admin" "Manage") + for appServer in ${appServers[@]}; do + log "configuring SSL for App Server $appServer" + curl --anyauth --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD \ + -X PUT -H "Content-type: application/json" -d '{"ssl-certificate-template":"defaultTemplate"}' \ + http://localhost:8002/manage/v2/servers/${appServer}/properties?group-id=${MARKLOGIC_GROUP} + sleep 5s + done + log "Info: Configure HTTPS in App Server finished" + + if [[ "$certType" == "self-signed" ]]; then + log "Info: Generate temporary certificate if necessary" + $AUTH_CURL -k -X POST -i -d @createTempCert.xqy -H "Content-type: application/x-www-form-urlencoded" \ + -H "Accept: multipart/mixed; boundary=BOUNDARY" https://localhost:8000/v1/eval + resp_code=$? + info "response code for Generate temporary certificate is $resp_code" + fi + fi + + log "Info: removing cert keys" + rm -f /run/secrets/marklogic-certs/*.key +} + +############################################################### +# Env Setup of MarkLogic +############################################################### +# Make sure username and password variables are not empty +if [[ -z "${MARKLOGIC_ADMIN_USERNAME}" ]] || [[ -z "${MARKLOGIC_ADMIN_PASSWORD}" ]]; then + error "MARKLOGIC_ADMIN_USERNAME and MARKLOGIC_ADMIN_PASSWORD must be set." exit +fi + +# generate JSON payload conditionally with license details. +if [[ -z "${LICENSE_KEY}" ]] || [[ -z "${LICENSEE}" ]]; then + LICENSE_PAYLOAD="{}" +else + info "LICENSE_KEY and LICENSEE are defined, installing MarkLogic license." + LICENSE_PAYLOAD="{\"license-key\" : \"${LICENSE_KEY}\",\"licensee\" : \"${LICENSEE}\"}" +fi + +# sets realm conditionally based on user input +if [[ -z "${REALM}" ]]; then + ML_REALM="public" +else + info "REALM is defined, setting realm." + ML_REALM="${REALM}" +fi + +if [[ -z "${MARKLOGIC_WALLET_PASSWORD}" ]]; then + MARKLOGIC_WALLET_PASSWORD_PAYLOAD="" +else + MARKLOGIC_WALLET_PASSWORD_PAYLOAD="wallet-password=${MARKLOGIC_WALLET_PASSWORD}" +fi + +############################################################### +info "Start configuring MarkLogic for $HOST_FQDN" +info "Bootstrap host: $MARKLOGIC_BOOTSTRAP_HOST" + +# Wait for current pod ready +wait_until_marklogic_ready $HOST_FQDN + +# Only do this if the bootstrap host is in the statefulset we are configuring +if [[ "${MARKLOGIC_CLUSTER_TYPE}" = "bootstrap" && "${HOST_FQDN}" = "${MARKLOGIC_BOOTSTRAP_HOST}" ]]; then + sleep 2s + init_security_db + configure_group +else + wait_bootstrap_ready + configure_group + join_cluster $HOST_FQDN +fi + +sleep 5s + +# Authentication configuration when path based is used +if [[ $POD_NAME == *-0 ]] && [[ $PATH_BASED_ROUTING == "true" ]]; then + log "Info: path based routing is set. Adapting authentication method" + resp=$(curl --anyauth -w "%{http_code}" --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s -X PUT -H "Content-type: application/json" -d '{"authentication":"basic"}' http://localhost:8002/manage/v2/servers/Admin/properties?group-id=${MARKLOGIC_GROUP}) + log "Info: Admin-Servers response code: $resp" + resp=$(curl --anyauth -w "%{http_code}" --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s -X PUT -H "Content-type: application/json" -d '{"authentication":"basic"}' http://localhost:8002/manage/v2/servers/App-Services/properties?group-id=${MARKLOGIC_GROUP}) + log "Info: App Service response code: $resp" + resp=$(curl --anyauth -w "%{http_code}" --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s -X PUT -H "Content-type: application/json" -d '{"authentication":"basic"}' http://localhost:8002/manage/v2/servers/Manage/properties?group-id=${MARKLOGIC_GROUP}) + log "Info: Manage response code: $resp" + log "Info: Default App-Servers authentication set to basic auth" +else + log "Info: This is not the boostrap host or path based routing is not set. Skipping authentication configuration" +fi +#End of authentication configuration + +if [[ $MARKLOGIC_JOIN_TLS_ENABLED == "true" ]]; then + configure_tls +fi + +info "helm script completed" +` +} diff --git a/pkg/k8sutil/handler.go b/pkg/k8sutil/handler.go index 3e07101..572b7db 100644 --- a/pkg/k8sutil/handler.go +++ b/pkg/k8sutil/handler.go @@ -11,6 +11,11 @@ func (oc *OperatorContext) ReconsileHandler() (reconcile.Result, error) { return result.Output() } setOperatorInternalStatus(oc, "Created") + + if result := oc.ReconcileConfigMap(); result.Completed() { + return result.Output() + } + result, err := oc.ReconcileStatefulset() return result, err diff --git a/pkg/k8sutil/statefulset.go b/pkg/k8sutil/statefulset.go index ab5d4c2..115ad9c 100644 --- a/pkg/k8sutil/statefulset.go +++ b/pkg/k8sutil/statefulset.go @@ -148,9 +148,10 @@ func generateStatefulSetsDef(stsMeta metav1.ObjectMeta, params statefulSetParame TypeMeta: generateTypeMeta("StatefulSet", "apps/v1"), ObjectMeta: stsMeta, Spec: appsv1.StatefulSetSpec{ - Selector: LabelSelectors(stsMeta.GetLabels()), - ServiceName: stsMeta.Name, - Replicas: params.Replicas, + Selector: LabelSelectors(stsMeta.GetLabels()), + ServiceName: stsMeta.Name, + Replicas: params.Replicas, + PodManagementPolicy: appsv1.ParallelPodManagement, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: stsMeta.GetLabels(), @@ -158,6 +159,7 @@ func generateStatefulSetsDef(stsMeta metav1.ObjectMeta, params statefulSetParame Spec: corev1.PodSpec{ Containers: generateContainerDef(stsMeta.GetName(), containerParams), TerminationGracePeriodSeconds: params.TerminationGracePeriodSeconds, + Volumes: generateVolumes(stsMeta.Name), }, }, }, @@ -195,6 +197,7 @@ func generateContainerDef(name string, containerParams containerParameters) []co Image: containerParams.Image, ImagePullPolicy: containerParams.ImagePullPolicy, Env: getEnvironmentVariables(containerParams), + Lifecycle: getLifeCycle(), ReadinessProbe: getReadinessProbe(), LivenessProbe: getLivenessProbe(), StartupProbe: getStartupProbe(), @@ -246,6 +249,37 @@ func generateContainerParams(cr *databasev1alpha1.MarklogicGroup) containerParam return containerParams } +func getLifeCycle() *corev1.Lifecycle { + return &corev1.Lifecycle{ + PostStart: &corev1.LifecycleHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"/bin/bash", "/tmp/helm-scripts/poststart-hook.sh"}, + }, + }, + PreStop: &corev1.LifecycleHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"/bin/bash", "/tmp/helm-scripts/prestop-hook.sh"}, + }, + }, + } +} + +func generateVolumes(stsName string) []corev1.Volume { + volumes := []corev1.Volume{} + volumes = append(volumes, corev1.Volume{ + Name: "helm-scripts", + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: fmt.Sprintf("%s-scripts", stsName), + }, + DefaultMode: func(i int32) *int32 { return &i }(0755), + }, + }, + }) + return volumes +} + func generatePVCTemplate(storageSize string) corev1.PersistentVolumeClaim { pvcTemplate := corev1.PersistentVolumeClaim{} pvcTemplate.CreationTimestamp = metav1.Time{} @@ -271,13 +305,16 @@ func getEnvironmentVariables(containerParams containerParameters) []corev1.EnvVa Value: fmt.Sprintf("%s.%s.svc.%s", containerParams.Name, containerParams.Namespace, containerParams.ClusterDomain), }, corev1.EnvVar{ Name: "MARKLOGIC_INIT", - Value: "true", + Value: "false", }, corev1.EnvVar{ Name: "MARKLOGIC_JOIN_CLUSTER", - Value: "true", + Value: "false", }, corev1.EnvVar{ Name: "MARKLOGIC_GROUP", Value: "Default", + }, corev1.EnvVar{ + Name: "MARKLOGIC_CLUSTER_TYPE", + Value: "bootstrap", }, ) if containerParams.LicenseKey != "" { @@ -309,11 +346,15 @@ func getVolumeMount() []corev1.VolumeMount { var VolumeMounts []corev1.VolumeMount // if persistenceEnabled != nil && *persistenceEnabled { - VolumeMounts = append(VolumeMounts, corev1.VolumeMount{ - Name: "data", - MountPath: "/var/opt/MarkLogic", - }) - + VolumeMounts = append(VolumeMounts, + corev1.VolumeMount{ + Name: "data", + MountPath: "/var/opt/MarkLogic", + }, + corev1.VolumeMount{ + Name: "helm-scripts", + MountPath: "/tmp/helm-scripts", + }) return VolumeMounts } From d28cda1899750eb27b200ee0fd76dd7668da37a4 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Tue, 4 Jun 2024 23:26:54 -0700 Subject: [PATCH 02/12] update configMap --- pkg/k8sutil/configmap.go | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pkg/k8sutil/configmap.go b/pkg/k8sutil/configmap.go index 13afc93..4a8f443 100644 --- a/pkg/k8sutil/configmap.go +++ b/pkg/k8sutil/configmap.go @@ -18,7 +18,6 @@ func (oc *OperatorContext) ReconcileConfigMap() result.ReconcileResult { annotations := map[string]string{} configMapName := cr.Spec.Name + "-scripts" objectMeta := generateObjectMeta(configMapName, cr.Namespace, labels, annotations) - namespace := objectMeta.Namespace nsName := types.NamespacedName{Name: objectMeta.Name, Namespace: objectMeta.Namespace} configmap := &corev1.ConfigMap{} err := client.Get(oc.Ctx, nsName, configmap) @@ -26,7 +25,7 @@ func (oc *OperatorContext) ReconcileConfigMap() result.ReconcileResult { if errors.IsNotFound(err) { logger.Info("MarkLogic sripts ConfigMap is not found, creating a new one") configmapDef := generateConfigMapDef(objectMeta, marklogicServerAsOwner(cr)) - err = oc.createConfigMap(namespace, configmapDef) + err = oc.createConfigMap(configmapDef) if err != nil { logger.Info("MarkLogic scripts configmap creation is failed") return result.Error(err) @@ -42,13 +41,13 @@ func (oc *OperatorContext) ReconcileConfigMap() result.ReconcileResult { return result.Continue() } -func generateConfigMapDef(serviceMeta metav1.ObjectMeta, ownerRef metav1.OwnerReference) *corev1.ConfigMap { +func generateConfigMapDef(configMapMeta metav1.ObjectMeta, ownerRef metav1.OwnerReference) *corev1.ConfigMap { configmap := &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ Kind: "ConfigMap", APIVersion: "v1", }, - ObjectMeta: serviceMeta, + ObjectMeta: configMapMeta, Data: map[string]string{ "liveness-probe.sh": getLivenessProbeScript(), "copy-certs.sh": getCopyCertScript(), @@ -60,7 +59,7 @@ func generateConfigMapDef(serviceMeta metav1.ObjectMeta, ownerRef metav1.OwnerRe return configmap } -func (oc *OperatorContext) createConfigMap(namespace string, configMap *corev1.ConfigMap) error { +func (oc *OperatorContext) createConfigMap(configMap *corev1.ConfigMap) error { logger := oc.ReqLogger client := oc.Client err := client.Create(oc.Ctx, configMap) From 54b338cce0dd1c73dd9d24f4b87328e0547dda3f Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Wed, 5 Jun 2024 01:14:57 -0700 Subject: [PATCH 03/12] use secret to store user credentials --- api/v1alpha1/common_types.go | 6 ++ api/v1alpha1/marklogicgroup_types.go | 1 + api/v1alpha1/zz_generated.deepcopy.go | 35 +++++++ ...abase.marklogic.com_marklogicclusters.yaml | 9 ++ ...atabase.marklogic.com_marklogicgroups.yaml | 9 ++ config/manager/kustomization.yaml | 6 ++ config/samples/marklogicgroup.yaml | 3 + pkg/k8sutil/common.go | 14 +++ pkg/k8sutil/configmap.go | 8 +- pkg/k8sutil/handler.go | 4 + pkg/k8sutil/secret.go | 94 +++++++++++++++++++ pkg/k8sutil/statefulset.go | 23 ++++- 12 files changed, 202 insertions(+), 10 deletions(-) create mode 100644 pkg/k8sutil/secret.go diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go index adc6837..860dd31 100644 --- a/api/v1alpha1/common_types.go +++ b/api/v1alpha1/common_types.go @@ -33,3 +33,9 @@ type VolumeMountWrapper struct { Volume []corev1.Volume `json:"volume,omitempty"` MountPath []corev1.VolumeMount `json:"mountPath,omitempty"` } + +type AdminAuth struct { + AdminUsername *string `json:"adminUsername,omitempty"` + AdminPassword *string `json:"adminPassword,omitempty"` + WalletPassword *string `json:"walletPassword,omitempty"` +} diff --git a/api/v1alpha1/marklogicgroup_types.go b/api/v1alpha1/marklogicgroup_types.go index f9ea2f6..3989e8f 100644 --- a/api/v1alpha1/marklogicgroup_types.go +++ b/api/v1alpha1/marklogicgroup_types.go @@ -38,6 +38,7 @@ type MarklogicGroupSpec struct { ImagePullPolicy string `json:"imagePullPolicy,omitempty"` ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + Auth *AdminAuth `json:"auth,omitempty"` Storage *Storage `json:"storage,omitempty"` Resources *corev1.ResourceRequirements `json:"resources,omitempty"` TerminationGracePeriodSeconds *int64 `json:"terminationGracePeriodSeconds,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 1e770c5..f9205f5 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -27,6 +27,36 @@ import ( runtime "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AdminAuth) DeepCopyInto(out *AdminAuth) { + *out = *in + if in.AdminUsername != nil { + in, out := &in.AdminUsername, &out.AdminUsername + *out = new(string) + **out = **in + } + if in.AdminPassword != nil { + in, out := &in.AdminPassword, &out.AdminPassword + *out = new(string) + **out = **in + } + if in.WalletPassword != nil { + in, out := &in.WalletPassword, &out.WalletPassword + *out = new(string) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AdminAuth. +func (in *AdminAuth) DeepCopy() *AdminAuth { + if in == nil { + return nil + } + out := new(AdminAuth) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GroupConfig) DeepCopyInto(out *GroupConfig) { *out = *in @@ -236,6 +266,11 @@ func (in *MarklogicGroupSpec) DeepCopyInto(out *MarklogicGroupSpec) { *out = make([]v1.LocalObjectReference, len(*in)) copy(*out, *in) } + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + *out = new(AdminAuth) + (*in).DeepCopyInto(*out) + } if in.Storage != nil { in, out := &in.Storage, &out.Storage *out = new(Storage) diff --git a/config/crd/bases/database.marklogic.com_marklogicclusters.yaml b/config/crd/bases/database.marklogic.com_marklogicclusters.yaml index 6f0f062..2439d46 100644 --- a/config/crd/bases/database.marklogic.com_marklogicclusters.yaml +++ b/config/crd/bases/database.marklogic.com_marklogicclusters.yaml @@ -972,6 +972,15 @@ spec: type: array type: object type: object + auth: + properties: + adminPassword: + type: string + adminUsername: + type: string + walletPassword: + type: string + type: object bootstrapHost: type: string clusterDomain: diff --git a/config/crd/bases/database.marklogic.com_marklogicgroups.yaml b/config/crd/bases/database.marklogic.com_marklogicgroups.yaml index 2e06f93..0789a63 100644 --- a/config/crd/bases/database.marklogic.com_marklogicgroups.yaml +++ b/config/crd/bases/database.marklogic.com_marklogicgroups.yaml @@ -862,6 +862,15 @@ spec: type: array type: object type: object + auth: + properties: + adminPassword: + type: string + adminUsername: + type: string + walletPassword: + type: string + type: object bootstrapHost: type: string clusterDomain: diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index 5c5f0b8..f3b8ec2 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -1,2 +1,8 @@ resources: - manager.yaml +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +images: +- name: controller + newName: pengzhouml/marklogic-kubernetes-operator + newTag: 0.3.1 diff --git a/config/samples/marklogicgroup.yaml b/config/samples/marklogicgroup.yaml index 7411cb6..8403012 100644 --- a/config/samples/marklogicgroup.yaml +++ b/config/samples/marklogicgroup.yaml @@ -12,6 +12,9 @@ spec: replicas: 1 name: marklogic image: "marklogicdb/marklogic-db:11.1.0-centos-1.1.2" + auth: + adminUsername: user + adminPassword: pass # storage: # size: 10Gi terminationGracePeriodSeconds: 9 diff --git a/pkg/k8sutil/common.go b/pkg/k8sutil/common.go index 9713cfc..c4fb4b3 100644 --- a/pkg/k8sutil/common.go +++ b/pkg/k8sutil/common.go @@ -3,7 +3,9 @@ package k8sutil import ( databasev1alpha1 "github.com/marklogic/marklogic-kubernetes-operator/api/v1alpha1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "math/rand" "sigs.k8s.io/controller-runtime/pkg/client" + "time" ) // generateTypeMeta generates the TyeMeta @@ -80,3 +82,15 @@ func setOperatorInternalStatus(oc *OperatorContext, newState databasev1alpha1.In return nil } + +func generateRandomAlphaNumeric(length int) string { + const charset = "abcdefghijklmnopqrstuvwxyz" + + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + + seededRand := rand.New(rand.NewSource(time.Now().UnixNano())) + result := make([]byte, length) + for i := range result { + result[i] = charset[seededRand.Intn(len(charset))] + } + return string(result) +} diff --git a/pkg/k8sutil/configmap.go b/pkg/k8sutil/configmap.go index 4a8f443..343c37a 100644 --- a/pkg/k8sutil/configmap.go +++ b/pkg/k8sutil/configmap.go @@ -108,7 +108,7 @@ fi func getCopyCertScript() string { return `#!/bin/bash MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" -MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/username)" +MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/password)" log () { local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") echo "${TIMESTAMP} $@" @@ -258,10 +258,8 @@ else echo "IS_BOOTSTRAP_HOST false" fi -# MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" -# MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/password)" -MARKLOGIC_ADMIN_USERNAME="admin" -MARKLOGIC_ADMIN_PASSWORD="admin" +MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" +MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/password)" pid=$(pgrep -fn start.marklogic) diff --git a/pkg/k8sutil/handler.go b/pkg/k8sutil/handler.go index 572b7db..5f42e57 100644 --- a/pkg/k8sutil/handler.go +++ b/pkg/k8sutil/handler.go @@ -12,6 +12,10 @@ func (oc *OperatorContext) ReconsileHandler() (reconcile.Result, error) { } setOperatorInternalStatus(oc, "Created") + if result := oc.ReconcileSecret(); result.Completed() { + return result.Output() + } + if result := oc.ReconcileConfigMap(); result.Completed() { return result.Output() } diff --git a/pkg/k8sutil/secret.go b/pkg/k8sutil/secret.go new file mode 100644 index 0000000..f323b7d --- /dev/null +++ b/pkg/k8sutil/secret.go @@ -0,0 +1,94 @@ +package k8sutil + +import ( + "github.com/marklogic/marklogic-kubernetes-operator/pkg/result" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +func (oc *OperatorContext) ReconcileSecret() result.ReconcileResult { + logger := oc.ReqLogger + client := oc.Client + cr := oc.MarklogicGroup + + logger.Info("Reconciling MarkLogic Secret") + labels := getMarkLogicLabels(cr.Spec.Name) + annotations := map[string]string{} + secretName := cr.Spec.Name + "-admin" + objectMeta := generateObjectMeta(secretName, cr.Namespace, labels, annotations) + nsName := types.NamespacedName{Name: objectMeta.Name, Namespace: objectMeta.Namespace} + secret := &corev1.Secret{} + err := client.Get(oc.Ctx, nsName, secret) + if err != nil { + if errors.IsNotFound(err) { + logger.Info("MarkLogic admin Secret is not found, creating a new one") + secretData := oc.generateSecretData() + secretDef := generateSecretDef(objectMeta, marklogicServerAsOwner(cr), secretData) + err = oc.createSecret(secretDef) + if err != nil { + logger.Info("MarkLogic admin Secret creation is failed") + return result.Error(err) + } + logger.Info("MarkLogic admin Secret creation is successful") + // result.Continue() + } else { + logger.Error(err, "MarkLogic admin Secret creation is failed") + return result.Error(err) + } + } + + return result.Continue() +} + +func (oc *OperatorContext) generateSecretData() map[string][]byte { + // logger := oc.ReqLogger + spec := oc.MarklogicGroup.Spec + secretData := map[string][]byte{} + if spec.Auth != nil && spec.Auth.AdminUsername != nil { + secretData["username"] = []byte(*spec.Auth.AdminUsername) + } else { + secretData["username"] = []byte(generateRandomAlphaNumeric(5)) + } + + if spec.Auth != nil && spec.Auth.AdminPassword != nil { + secretData["password"] = []byte(*spec.Auth.AdminPassword) + } else { + secretData["password"] = []byte(generateRandomAlphaNumeric(10)) + } + + if spec.Auth != nil && spec.Auth.WalletPassword != nil { + secretData["wallet-password"] = []byte(*spec.Auth.WalletPassword) + } else { + secretData["wallet-password"] = []byte(generateRandomAlphaNumeric(10)) + } + + return secretData +} + +func generateSecretDef(secretMeta metav1.ObjectMeta, ownerRef metav1.OwnerReference, secretData map[string][]byte) *corev1.Secret { + secret := &corev1.Secret{ + TypeMeta: metav1.TypeMeta{ + Kind: "Secret", + APIVersion: "v1", + }, + ObjectMeta: secretMeta, + Type: corev1.SecretTypeOpaque, + Data: secretData, + } + secret.SetOwnerReferences(append(secret.GetOwnerReferences(), ownerRef)) + return secret +} + +func (oc *OperatorContext) createSecret(secret *corev1.Secret) error { + logger := oc.ReqLogger + client := oc.Client + err := client.Create(oc.Ctx, secret) + if err != nil { + logger.Error(err, "MarkLogic admin secret creation is failed") + return err + } + logger.Info("MarkLogic script admin secret is successful") + return nil +} diff --git a/pkg/k8sutil/statefulset.go b/pkg/k8sutil/statefulset.go index 115ad9c..d8c840d 100644 --- a/pkg/k8sutil/statefulset.go +++ b/pkg/k8sutil/statefulset.go @@ -276,6 +276,13 @@ func generateVolumes(stsName string) []corev1.Volume { DefaultMode: func(i int32) *int32 { return &i }(0755), }, }, + }, corev1.Volume{ + Name: "mladmin-secrets", + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: fmt.Sprintf("%s-admin", stsName), + }, + }, }) return volumes } @@ -295,11 +302,11 @@ func generatePVCTemplate(storageSize string) corev1.PersistentVolumeClaim { func getEnvironmentVariables(containerParams containerParameters) []corev1.EnvVar { envVars := []corev1.EnvVar{} envVars = append(envVars, corev1.EnvVar{ - Name: "MARKLOGIC_ADMIN_USERNAME", - Value: "admin", + Name: "MARKLOGIC_ADMIN_USERNAME_FILE", + Value: "ml-secrets/username", }, corev1.EnvVar{ - Name: "MARKLOGIC_ADMIN_PASSWORD", - Value: "admin", + Name: "MARKLOGIC_ADMIN_PASSWORD_FILE", + Value: "ml-secrets/password", }, corev1.EnvVar{ Name: "MARKLOGIC_FQDN_SUFFIX", Value: fmt.Sprintf("%s.%s.svc.%s", containerParams.Name, containerParams.Namespace, containerParams.ClusterDomain), @@ -354,7 +361,13 @@ func getVolumeMount() []corev1.VolumeMount { corev1.VolumeMount{ Name: "helm-scripts", MountPath: "/tmp/helm-scripts", - }) + }, + corev1.VolumeMount{ + Name: "mladmin-secrets", + MountPath: "/run/secrets/ml-secrets", + ReadOnly: true, + }, + ) return VolumeMounts } From 68d80b938516dc6779aca59ecadd1912e0f27661 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Wed, 5 Jun 2024 08:57:58 -0700 Subject: [PATCH 04/12] update default value --- api/v1alpha1/marklogicgroup_types.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/v1alpha1/marklogicgroup_types.go b/api/v1alpha1/marklogicgroup_types.go index 3989e8f..54dbb38 100644 --- a/api/v1alpha1/marklogicgroup_types.go +++ b/api/v1alpha1/marklogicgroup_types.go @@ -34,7 +34,9 @@ type MarklogicGroupSpec struct { // +kubebuilder:default:="cluster.local" ClusterDomain string `json:"clusterDomain,omitempty"` + // +kubebuilder:default:="marklogicdb/marklogic-db:11.2.0-ubi" Image string `json:"image"` + // +kubebuilder:default:="IfNotPresent" ImagePullPolicy string `json:"imagePullPolicy,omitempty"` ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` @@ -45,7 +47,7 @@ type MarklogicGroupSpec struct { // +kubebuilder:validation:Enum=OnDelete;RollingUpdate // +kubebuilder:default:="OnDelete" UpdateStrategy string `json:"updateStrategy,omitempty"` - PodManagementPolicy *string `json:"podManagementPolicy,omitempty"` + // PodManagementPolicy *string `json:"podManagementPolicy,omitempty"` NetworkPolicy *networkingv1.NetworkPolicy `json:"networkPolicy,omitempty"` PodSecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` ContainerSecurityContext *corev1.SecurityContext `json:"containerSecurityContext,omitempty"` From b3cfa5d186f2412199a2733af054018da6c84eb0 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Wed, 5 Jun 2024 17:07:16 -0700 Subject: [PATCH 05/12] add default values --- .../crd/bases/database.marklogic.com_marklogicclusters.yaml | 4 ++-- config/crd/bases/database.marklogic.com_marklogicgroups.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/crd/bases/database.marklogic.com_marklogicclusters.yaml b/config/crd/bases/database.marklogic.com_marklogicclusters.yaml index 2439d46..466cd2a 100644 --- a/config/crd/bases/database.marklogic.com_marklogicclusters.yaml +++ b/config/crd/bases/database.marklogic.com_marklogicclusters.yaml @@ -1173,8 +1173,10 @@ spec: type: string type: object image: + default: marklogicdb/marklogic-db:11.2.0-ubi type: string imagePullPolicy: + default: IfNotPresent type: string imagePullSecrets: items: @@ -1967,8 +1969,6 @@ spec: additionalProperties: type: string type: object - podManagementPolicy: - type: string priorityClassName: type: string readinessProbe: diff --git a/config/crd/bases/database.marklogic.com_marklogicgroups.yaml b/config/crd/bases/database.marklogic.com_marklogicgroups.yaml index 0789a63..6e192bc 100644 --- a/config/crd/bases/database.marklogic.com_marklogicgroups.yaml +++ b/config/crd/bases/database.marklogic.com_marklogicgroups.yaml @@ -1048,8 +1048,10 @@ spec: type: string type: object image: + default: marklogicdb/marklogic-db:11.2.0-ubi type: string imagePullPolicy: + default: IfNotPresent type: string imagePullSecrets: items: @@ -1751,8 +1753,6 @@ spec: additionalProperties: type: string type: object - podManagementPolicy: - type: string priorityClassName: type: string readinessProbe: From 1bd0b1776bd61f2c61ebd6ed9e6a39b05453a653 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Wed, 5 Jun 2024 17:07:30 -0700 Subject: [PATCH 06/12] remove podManagementPolicy --- api/v1alpha1/marklogicgroup_types.go | 3 +-- api/v1alpha1/zz_generated.deepcopy.go | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/api/v1alpha1/marklogicgroup_types.go b/api/v1alpha1/marklogicgroup_types.go index 54dbb38..c006eb7 100644 --- a/api/v1alpha1/marklogicgroup_types.go +++ b/api/v1alpha1/marklogicgroup_types.go @@ -35,7 +35,7 @@ type MarklogicGroupSpec struct { ClusterDomain string `json:"clusterDomain,omitempty"` // +kubebuilder:default:="marklogicdb/marklogic-db:11.2.0-ubi" - Image string `json:"image"` + Image string `json:"image"` // +kubebuilder:default:="IfNotPresent" ImagePullPolicy string `json:"imagePullPolicy,omitempty"` ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` @@ -47,7 +47,6 @@ type MarklogicGroupSpec struct { // +kubebuilder:validation:Enum=OnDelete;RollingUpdate // +kubebuilder:default:="OnDelete" UpdateStrategy string `json:"updateStrategy,omitempty"` - // PodManagementPolicy *string `json:"podManagementPolicy,omitempty"` NetworkPolicy *networkingv1.NetworkPolicy `json:"networkPolicy,omitempty"` PodSecurityContext *corev1.PodSecurityContext `json:"securityContext,omitempty"` ContainerSecurityContext *corev1.SecurityContext `json:"containerSecurityContext,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f9205f5..745b3e7 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -286,11 +286,6 @@ func (in *MarklogicGroupSpec) DeepCopyInto(out *MarklogicGroupSpec) { *out = new(int64) **out = **in } - if in.PodManagementPolicy != nil { - in, out := &in.PodManagementPolicy, &out.PodManagementPolicy - *out = new(string) - **out = **in - } if in.NetworkPolicy != nil { in, out := &in.NetworkPolicy, &out.NetworkPolicy *out = new(networkingv1.NetworkPolicy) From ed5c5df1a8950220fbf3a47f328ba706164aaf2f Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Wed, 5 Jun 2024 22:09:53 -0700 Subject: [PATCH 07/12] refactor the way to load script to configMap --- pkg/k8sutil/configmap.go | 873 +------------------------- pkg/k8sutil/scripts/copy-certs.sh | 69 ++ pkg/k8sutil/scripts/liveness-probe.sh | 31 + pkg/k8sutil/scripts/poststart-hook.sh | 674 ++++++++++++++++++++ pkg/k8sutil/scripts/prestop-hook.sh | 54 ++ 5 files changed, 855 insertions(+), 846 deletions(-) create mode 100644 pkg/k8sutil/scripts/copy-certs.sh create mode 100644 pkg/k8sutil/scripts/liveness-probe.sh create mode 100644 pkg/k8sutil/scripts/poststart-hook.sh create mode 100644 pkg/k8sutil/scripts/prestop-hook.sh diff --git a/pkg/k8sutil/configmap.go b/pkg/k8sutil/configmap.go index 343c37a..4893033 100644 --- a/pkg/k8sutil/configmap.go +++ b/pkg/k8sutil/configmap.go @@ -1,6 +1,7 @@ package k8sutil import ( + "embed" "github.com/marklogic/marklogic-kubernetes-operator/pkg/result" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -8,6 +9,9 @@ import ( "k8s.io/apimachinery/pkg/types" ) +//go:embed scripts/* +var scriptsFolder embed.FS + func (oc *OperatorContext) ReconcileConfigMap() result.ReconcileResult { logger := oc.ReqLogger client := oc.Client @@ -24,7 +28,7 @@ func (oc *OperatorContext) ReconcileConfigMap() result.ReconcileResult { if err != nil { if errors.IsNotFound(err) { logger.Info("MarkLogic sripts ConfigMap is not found, creating a new one") - configmapDef := generateConfigMapDef(objectMeta, marklogicServerAsOwner(cr)) + configmapDef := oc.generateConfigMapDef(objectMeta, marklogicServerAsOwner(cr)) err = oc.createConfigMap(configmapDef) if err != nil { logger.Info("MarkLogic scripts configmap creation is failed") @@ -41,19 +45,16 @@ func (oc *OperatorContext) ReconcileConfigMap() result.ReconcileResult { return result.Continue() } -func generateConfigMapDef(configMapMeta metav1.ObjectMeta, ownerRef metav1.OwnerReference) *corev1.ConfigMap { +func (oc *OperatorContext) generateConfigMapDef(configMapMeta metav1.ObjectMeta, ownerRef metav1.OwnerReference) *corev1.ConfigMap { + + configMapData := oc.getScriptsForConfigMap() configmap := &corev1.ConfigMap{ TypeMeta: metav1.TypeMeta{ Kind: "ConfigMap", APIVersion: "v1", }, ObjectMeta: configMapMeta, - Data: map[string]string{ - "liveness-probe.sh": getLivenessProbeScript(), - "copy-certs.sh": getCopyCertScript(), - "prestop-hook.sh": getPrestopHookScript(), - "poststart-hook.sh": getPoststartHookScript(), - }, + Data: configMapData, } configmap.SetOwnerReferences(append(configmap.GetOwnerReferences(), ownerRef)) return configmap @@ -71,843 +72,23 @@ func (oc *OperatorContext) createConfigMap(configMap *corev1.ConfigMap) error { return nil } -func getLivenessProbeScript() string { - return `#!/bin/bash -log () { - local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") - # Check to make sure pod doesn't terminate if PID value is empty for any reason - if [ -n "$pid" ]; then - echo "${TIMESTAMP} $@" > /proc/$pid/fd/1 - fi -} - -pid=$(pgrep -fn start.marklogic) - -# Check if ML service is running. Exit with 1 if it is other than running -ml_status=$(/etc/init.d/MarkLogic status) - -if [[ "$ml_status" =~ "running" ]]; then - http_code=$(curl -o /tmp/probe_response.txt -s -w "%{http_code}" "http://${HOSTNAME}:8001/admin/v1/timestamp") - curl_code=$? - http_resp=$(cat /tmp/probe_response.txt) - - if [[ $curl_code -ne 0 && $http_code -ne 401 ]]; then - log "Info: [Liveness Probe] Error with MarkLogic" - log "Info: [Liveness Probe] Curl response code: "$curl_code - log "Info: [Liveness Probe] Http response code: "$http_code - log "Info: [Liveness Probe] Http response message: "$http_resp - fi - rm -f /tmp/probe_response.txt - exit 0 -else - exit 1 -fi -` -} - -func getCopyCertScript() string { - return `#!/bin/bash -MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" -MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/password)" -log () { - local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") - echo "${TIMESTAMP} $@" -} -if [[ -d "/tmp/server-cert-secrets" ]]; then - certType="named" -else - certType="self-signed" -fi -log "Info: [copy-certs] Proceeding with $certType certificate flow." -host_FQDN="$POD_NAME.$MARKLOGIC_FQDN_SUFFIX" -log "Info: [copy-certs] FQDN for this server: $host_FQDN" -foundMatchingCert="false" -if [[ "$certType" == "named" ]]; then - cp -f /tmp/ca-cert-secret/* /run/secrets/marklogic-certs/; - cert_paths=$(find /tmp/server-cert-secrets/tls_*.crt) - for cert_path in $cert_paths; do - cert_cn=$(openssl x509 -noout -subject -in $cert_path | sed -n 's/.*CN = \([^,]*\).*/\1/p') - log "Info: [copy-certs] FQDN for the certificate: $cert_cn" - if [[ "$host_FQDN" == "$cert_cn" ]]; then - log "Info: [copy-certs] found certificate for the server" - foundMatchingCert="true" - cp $cert_path /run/secrets/marklogic-certs/tls.crt - pkey_path=$(echo "$cert_path" | sed "s:.crt:.key:") - cp $pkey_path /run/secrets/marklogic-certs/tls.key - if [[ ! -e "$pkey_path" ]]; then - log "Error: [copy-certs] private key tls.key for certificate $cert_cn is not found. Exiting." - exit 1 - fi - - # verify the tls.crt and cacert.pem is valid, otherwise exit - openssl verify -CAfile /run/secrets/marklogic-certs/cacert.pem /run/secrets/marklogic-certs/tls.crt - if [[ $? -ne 0 ]]; then - log "Error: [copy-certs] Server certificate tls.crt verification with cacert.pem failed. Exiting." - exit 1 - fi - # verify the tls.crt and tls.key is matching, otherwise exit - privateKeyMD5=$(openssl rsa -modulus -noout -in /run/secrets/marklogic-certs/tls.key | openssl md5) - publicKeyMD5=$(openssl x509 -modulus -noout -in /run/secrets/marklogic-certs/tls.crt | openssl md5) - if [[ -z "privateKeyMD5" ]] || [[ "$privateKeyMD5" != "$publicKeyMD5" ]]; then - log "Error: [copy-certs] private key tls.key and server certificate tls.crt are not matching. Exiting." - exit 1 - fi - log "Info: [copy-certs] certificate and private key are valid." - break - fi - done - if [[ $foundMatchingCert == "false" ]]; then - if [[ $POD_NAME = *"-0" ]]; then - log "Error: [copy-certs] Failed to find matching certificate for the bootstrap server. Exiting." - exit 1 - else - log "Error: [copy-certs] Failed to find matching certificate for the non-bootstrap server. Continuing with temporary certificate for this host. Please update the certificate for this host later." - fi - fi -elif [[ "$certType" == "self-signed" ]]; then - if [[ $POD_NAME != *"-0" ]] || [[ $MARKLOGIC_CLUSTER_TYPE == "non-bootstrap" ]]; then - log "Info: [copy-certs] Getting CA for bootstrap host" - cd /run/secrets/marklogic-certs/ - echo quit | openssl s_client -showcerts -servername "${MARKLOGIC_BOOTSTRAP_HOST}" -showcerts -connect "${MARKLOGIC_BOOTSTRAP_HOST}":8000 2>&1 < /dev/null | sed -n '/-----BEGIN/,/-----END/p' > cacert.pem - fi -else - log "Error: [copy-certs] unknown certType: $certType" - exit 1 -fi -` -} - -func getPrestopHookScript() string { - return `#!/bin/bash -MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" -MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/password)" - -log () { - local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") - # Check to make sure pod doesn't terminate if PID value is empty for any reason - # If PID value is empty preStart hook logs are not recorded - if [ -n "$pid" ]; then - echo "${TIMESTAMP} $@" > /proc/$pid/fd/1 - fi -} - -pid=$(pgrep -fn start.marklogic) -log "Info: [prestop] Prestop Hook Execution" - -my_host=$(hostname -f) - -HTTP_PROTOCOL="http" -HTTPS_OPTION="" -if [[ "$MARKLOGIC_JOIN_TLS_ENABLED" == "true" ]]; then - HTTP_PROTOCOL="https" - HTTPS_OPTION="-k" -fi -log "Info: [prestop] MarkLogic Pod Hostname: "$my_host -for ((i = 0; i < 5; i = i + 1)); do - res_code=$(curl --anyauth --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD \ - -o /dev/null -m 10 -s -w %{http_code} \ - -i -X POST ${HTTPS_OPTION} --data "state=shutdown&failover=true" \ - -H "Content-type: application/x-www-form-urlencoded" \ - ${HTTP_PROTOCOL}://localhost:8002/manage/v2/hosts/$my_host?format=json) - - if [[ ${res_code} -eq 202 ]]; then - log "Info: [prestop] Host shut down response code: "$res_code - - while (true) - do - ml_status=$(service MarkLogic status) - log "Info: [prestop] MarkLogic Status: "$ml_status - if [[ "$ml_status" =~ "running" ]]; then - sleep 5s - continue - else - break - fi - done - break - else - log "ERROR: [prestop] Retry Attempt: "$i - log "ERROR: [prestop] Host shut down expected response code 202, got "$res_code - sleep 10s - fi -done -` -} - -func getPoststartHookScript() string { - return `#!/bin/bash -# Refer to https://docs.marklogic.com/guide/admin-api/cluster#id_10889 for cluster joining process - -N_RETRY=60 -RETRY_INTERVAL=1 -HOST_FQDN="$(hostname).${MARKLOGIC_FQDN_SUFFIX}" - -# HTTP_PROTOCOL could be http or https -HTTP_PROTOCOL="http" -HTTPS_OPTION="" -if [[ "$MARKLOGIC_JOIN_TLS_ENABLED" == "true" ]]; then - HTTP_PROTOCOL="https" - HTTPS_OPTION="-k" -fi - -IS_BOOTSTRAP_HOST=false -if [[ "$(hostname)" == *-0 ]]; then - echo "IS_BOOTSTRAP_HOST true" - IS_BOOTSTRAP_HOST=true -else - echo "IS_BOOTSTRAP_HOST false" -fi - -MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" -MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/password)" - -pid=$(pgrep -fn start.marklogic) - -############################################################### -# Logging utility -############################################################### -info() { - log "Info" "$@" -} - -error() { - log "Error" "$1" - local EXIT_STATUS="$2" - if [[ ${EXIT_STATUS} == "exit" ]] - then - exit 1 - fi -} - -log () { - local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") - # Check to make sure pod doesn't terminate if PID value is empty for any reason - # If PID value is empty postStart hook logs are not recorded - message="${TIMESTAMP} [postStart] $@" - if [ -n "$pid" ]; then - echo $message > /proc/$pid/fd/1 - fi - - echo $message >> /tmp/script.log -} - -################################################################ -# restart_check(hostname, baseline_timestamp) -# -# Use the timestamp service to detect a server restart, given a -# a baseline timestamp. Use N_RETRY and RETRY_INTERVAL to tune -# the test length. Include authentication in the curl command -# so the function works whether or not security is initialized. -# $1 : The hostname to test against -# $2 : The baseline timestamp -# Returns 0 if restart is detected, exits with an error if not. -################################################################ -function restart_check { - local hostname=$1 - local old_timestamp=$2 - local retry_count - local last_start - - info "${hostname} - waiting for MarkLogic to restart" - - last_start=$( \ - curl -s --anyauth \ - --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ - "http://${hostname}:8001/admin/v1/timestamp" \ - ) - for ((retry_count = 0; retry_count < N_RETRY; retry_count = retry_count + 1)); do - if [ "${old_timestamp}" == "${last_start}" ] || [ -z "${last_start}" ]; then - info "${hostname} - waiting for MarkLogic to restart: ${old_timestamp} ${last_start}" - sleep ${RETRY_INTERVAL} - last_start=$( \ - curl -s --anyauth \ - --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ - "http://${hostname}:8001/admin/v1/timestamp" \ - ) - else - info "${hostname} - MarkLogic has restarted" - return 0 - fi - done - error "${hostname} - failed to restart" exit -} -################################################################ -# retry_and_timeout(target_url, expected_response_code, additional_options, return_error) -# The third argument is optional and can be used to pass additional options to curl. -# Fourth argurment is optional, default is set to true, can be used when custom error handling is required, -# if set to true means function will return error and exit if curl fails N_RETRY times -# setting to false means function will return response code instead of failing and exiting. -# Retry a curl command until it returns the expected response -# code or fails N_RETRY times. -# Use RETRY_INTERVAL to tune the test length. -# Validate that response code is the same as expected response -# code or exit with an error. -# -# $1 : The target url to test against -# $2 : The expected response code -# $3 : Additional options to pass to curl -# $4 : Option to return error or response code in case of error -################################################################ -function curl_retry_validate { - local retry_count - local return_error="${4:-true}" - for ((retry_count = 0; retry_count < N_RETRY; retry_count = retry_count + 1)); do - request="curl -m 30 -s -w '%{http_code}' $3 $1" - response_code=$(eval "${request}") - if [[ ${response_code} -eq $2 ]]; then - return "${response_code}" - fi - sleep ${RETRY_INTERVAL} - done - if [[ "${return_error}" = "false" ]] ; then - return "${response_code}" - fi - error "Expected response code ${2}, got ${response_code} from ${1}." exit -} - -################################################################ -# Function to initialize a host -# $1: The host name -# return values: 0 - successfully initialized -# 1 - host not reachable -################################################################ -function wait_until_marklogic_ready { - local host=$1 - info "wait until $host is ready" - timestamp=$( curl -s --anyauth \ - --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ - http://${host}:8001/admin/v1/timestamp ) - if [ -z "${timestamp}" ]; then - info "${host} - not responding yet" - sleep 5s - wait_until_marklogic_ready $host - else - info "${host} - responding, calling init" - out="/tmp/${host}.out" - - response_code=$( \ - curl --anyauth -m 30 -s --retry 5 \ - -w '%{http_code}' -o "${out}" \ - -i -X POST -H "Content-type:application/json" \ - -d "${LICENSE_PAYLOAD}" \ - --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ - http://${host}:8001/admin/v1/init \ - ) - if [ "${response_code}" = "202" ]; then - info "${host} - init called, restart triggered" - last_startup=$( \ - cat "${out}" | - grep "last-startup" | - sed 's%^.*\(.*\).*$%\1%' \ - ) - - restart_check "${host}" "${last_startup}" - info "${host} - restarted" - info "${host} - init complete" - elif [ "${response_code}" -eq "204" ]; then - info "${host} - init called, no restart triggered" - info "${host} - init complete" - else - info "${host} - error calling init: ${response_code}" - fi - fi -} - -################################################################ -# Function to initialize a host -# $1: The host name -# return values: 0 - successfully initialized -# 1 - host not reachable -################################################################ -function init_marklogic_host { - local hostname=$1 - info "initializing host: $hostname" - timestamp=$( \ - curl -s --anyauth \ - --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ - http://${hostname}:8001/admin/v1/timestamp \ - ) - if [ -z "${timestamp}" ]; then - info "${hostname} - not responding yet" - return 1 - fi - info "${hostname} - responding, calling init" - output_path="/tmp/${hostname}.out" - response_code=$( \ - curl --anyauth -m 30 -s --retry 5 \ - -w '%{http_code}' -o "${output_path}" \ - -i -X POST -H "Content-type:application/json" \ - -d "${LICENSE_PAYLOAD}" \ - --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ - http://${hostname}:8001/admin/v1/init \ - ) - - if [ "${response_code}" = "202" ]; then - info "${hostname} - init called, restart triggered" - last_startup=$( \ - cat "${output_path}" | - grep "last-startup" | - sed 's%^.*\(.*\).*$%\1%' \ - ) - - restart_check "${hostname}" "${last_startup}" - return 0 - elif [ "${response_code}" -eq "204" ]; then - info "${hostname} - init called, no restart triggered" - info "${hostname} - init complete" - return 0 - else - info "${hostname} - error calling init: ${response_code}" - [ -f "${out}" ] && cat "${out}" - fi -} - -################################################################ -# Function to bootstrap host is ready: -# 1. If TLS is not enabled, wait until Security DB is installed. -# 2. If TLS is enabled, wait until TLS is turned on in App Server -# return values: 0 - admin user successfully initialized -################################################################ -function wait_bootstrap_ready { - resp=$(curl -w '%{http_code}' -o /dev/null http://$MARKLOGIC_BOOTSTRAP_HOST:8001/admin/v1/timestamp ) - if [[ "$MARKLOGIC_JOIN_TLS_ENABLED" == "true" ]]; then - # return 403 if tls is enabled - if [[ $resp -eq 403 ]]; then - info "Bootstrap host is ready with TLS enabled" - else - info "Timestamp response code:$resp. Bootstrap host is not ready with TLS enabled, try again in 10s" - sleep 10s - wait_bootstrap_ready - fi - else - if [[ $resp -eq 401 ]]; then - info "Bootstrap host is ready with no TLS" - else - info "Timestamp response code:$resp. Bootstrap host is not ready, try again in 10s" - sleep 10s - wait_bootstrap_ready - fi - fi -} - -################################################################ -# Function to initialize admin user and security DB -# -# return values: 0 - admin user successfully initialized -################################################################ -function init_security_db { - info "initializing as bootstrap cluster" - - # check to see if the bootstrap host is already configured - response_code=$( \ - curl -s --anyauth \ - -w '%{http_code}' -o "/tmp/${MARKLOGIC_BOOTSTRAP_HOST}.out" \ - --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" $HTTPS_OPTION \ - $HTTP_PROTOCOL://$MARKLOGIC_BOOTSTRAP_HOST:8002/manage/v2/hosts/$MARKLOGIC_BOOTSTRAP_HOST/properties - ) - - if [ "${response_code}" = "200" ]; then - info "${MARKLOGIC_BOOTSTRAP_HOST} - bootstrap security already initialized" - return 0 - else - info "${MARKLOGIC_BOOTSTRAP_HOST} - initializing bootstrap security" - - # Get last restart timestamp directly before instance-admin call to verify restart after - timestamp=$( \ - curl -s --anyauth \ - --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ - "http://${MARKLOGIC_BOOTSTRAP_HOST}:8001/admin/v1/timestamp" \ - ) - - curl_retry_validate "http://${MARKLOGIC_BOOTSTRAP_HOST}:8001/admin/v1/instance-admin" 202 \ - "-o /dev/null \ - -X POST -H \"Content-type:application/x-www-form-urlencoded; charset=utf-8\" \ - -d \"admin-username=${MARKLOGIC_ADMIN_USERNAME}\" --data-urlencode \"admin-password=${MARKLOGIC_ADMIN_PASSWORD}\" \ - -d \"realm=${ML_REALM}\" -d \"${MARKLOGIC_WALLET_PASSWORD_PAYLOAD}\"" - - restart_check "${MARKLOGIC_BOOTSTRAP_HOST}" "${timestamp}" - - info "${MARKLOGIC_BOOTSTRAP_HOST} - bootstrap security initialized" - return 0 - fi -} - -################################################################ -# Function to join marklogic host to cluster -# -# return values: 0 - admin user successfully initialized -################################################################ -function join_cluster { - hostname=$1 - - # check if Bootstrap Host is ready - # if server could not be reached, response_code == 000 - # if host has not join cluster, return 404 - # if bootstrap host not init, return 403 - # if Security DB not set or credential not correct return 401 - # if host is already in cluster, return 200 - response_code=$( curl -s --anyauth -o /dev/null -w '%{http_code}' \ - --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" $HTTPS_OPTION \ - $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/hosts/${hostname}/properties?format=xml \ - ) - - info "response_code: $response_code" - - if [ "${response_code}" = "200" ]; then - info "host has already joined the cluster" - return 0 - elif [ "${response_code}" != "404" ]; then - sleep 10s - join_cluster $hostname - else - info "Proceed to joining bootstrap host" - fi - - # process to join the host - - # Wait until the group is ready - retry_count=10 - while [ $retry_count -gt 0 ]; do - GROUP_RESP_CODE=$( curl --anyauth -m 20 -s -o /dev/null -w "%{http_code}" $HTTPS_OPTION -X GET $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups/${MARKLOGIC_GROUP} --anyauth --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} ) - if [[ ${GROUP_RESP_CODE} -eq 200 ]]; then - info "Found the group, process to join the group" - break - else - ((retry_count--)) - info "GROUP_RESP_CODE: $GROUP_RESP_CODE , retry $retry_count times to joining ${MARKLOGIC_GROUP} group in marklogic cluster" - sleep 10s - fi - done - - if [[ $retry_count -le 0 ]]; then - info "retry_count: $retry_count" - error "pass timeout to wait for the group ready" - exit 1 - fi - - info "${hostname} - joining group ${MARKLOGIC_GROUP}" - payload=\"group=${MARKLOGIC_GROUP}\" - curl_retry_validate "http://${hostname}:8001/admin/v1/server-config" 200 \ - "--anyauth --user \"${MARKLOGIC_ADMIN_USERNAME}\":\"${MARKLOGIC_ADMIN_PASSWORD}\" \ - -o /tmp/${hostname}.xml -X GET -H \"Accept: application/xml\"" - - curl_retry_validate "$HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8001/admin/v1/cluster-config" 200 \ - "--anyauth $HTTPS_OPTION --user \"${MARKLOGIC_ADMIN_USERNAME}\":\"${MARKLOGIC_ADMIN_PASSWORD}\" \ - -X POST -d \"${payload}\" \ - --data-urlencode \"server-config@/tmp/${hostname}.xml\" \ - -H \"Content-type: application/x-www-form-urlencoded\" \ - -o /tmp/${hostname}_cluster.zip" - - timestamp=$( \ - curl -s --anyauth $HTTPS_OPTION \ - --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ - "http://${hostname}:8001/admin/v1/timestamp" \ - ) - - curl_retry_validate "http://${hostname}:8001/admin/v1/cluster-config" 202 \ - "-o /dev/null --anyauth --user \"${MARKLOGIC_ADMIN_USERNAME}\":\"${MARKLOGIC_ADMIN_PASSWORD}\" \ - -X POST -H \"Content-type: application/zip\" \ - --data-binary @/tmp/${hostname}_cluster.zip" - - # 202 causes restart - info "${hostname} - restart triggered" - # restart_check "${hostname}" "${timestamp}" - - info "${hostname} - joined group ${MARKLOGIC_GROUP}" -} - -################################################################ -# Function to configure MarkLogic Group -# -# return -################################################################ -function configure_group { - - if [[ "$IS_BOOTSTRAP_HOST" == "true" ]]; then - group_cfg_template='{"group-name":"%s", "xdqp-ssl-enabled":"%s"}' - group_cfg=$(printf "$group_cfg_template" "$MARKLOGIC_GROUP" "$XDQP_SSL_ENABLED") - - # check if host is already in and get the current cluster - response_code=$( \ - curl -s --anyauth \ - -w '%{http_code}' -o "/tmp/groups.out" \ - --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ - http://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/hosts/${HOST_FQDN}/properties?format=xml - ) - - if [ "${response_code}" = "200" ]; then - current_group=$( \ - cat "/tmp/groups.out" | - grep "group" | - sed 's%^.*\(.*\).*$%\1%' \ - ) - - info "current_group: $current_group" - info "group_cfg: $group_cfg" - - # curl retry doesn't work in the lower version - response_code=$( \ - curl -s --anyauth \ - --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} \ - -w '%{http_code}' \ - -X PUT \ - -H "Content-type: application/json" \ - -d "${group_cfg}" \ - http://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups/${current_group}/properties \ - ) - - info "response_code: $response_code" - - if [[ "${response_code}" = "204" ]]; then - info "group \"${current_group}\" updated" - elif [[ "${response_code}" = "202" ]]; then - # Note: THIS SHOULD NOT HAPPEN WITH THE CURRENT GROUP CONFIG - info "group \"${current_group}\" updated and a restart of all hosts in the group was triggered" - else - info "unexpected response when updating group \"${current_group}\": ${response_code}" - fi - - fi - - if [[ "$MARKLOGIC_CLUSTER_TYPE" == "non-bootstrap" ]]; then - info "creating group for other Helm Chart" - - # Create a group if group is not already exits - GROUP_RESP_CODE=$( curl --anyauth -m 20 -s -o /dev/null -w "%{http_code}" $HTTPS_OPTION -X GET $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups/${MARKLOGIC_GROUP} --anyauth --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} ) - if [[ ${GROUP_RESP_CODE} -eq 200 ]]; then - info "Skipping creation of group $MARKLOGIC_GROUP as it already exists on the MarkLogic cluster." - else - res_code=$(curl --anyauth --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} $HTTPS_OPTION -m 20 -s -w '%{http_code}' -X POST -d "${group_cfg}" -H "Content-type: application/json" $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups) - if [[ ${res_code} -eq 201 ]]; then - log "Info: [initContainer] Successfully configured group $MARKLOGIC_GROUP on the MarkLogic cluster." - else - log "Info: [initContainer] Expected response code 201, got $res_code" - fi - fi - - fi - else - info "not bootstrap host. Skip group configuration" - fi - -} - -function configure_tls { - info "Configuring TLS for App Servers" - - AUTH_CURL="curl --anyauth --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s " - - cd /tmp/ - if [[ -e "/run/secrets/marklogic-certs/tls.crt" ]]; then - info "Configuring named certificates on host" - certType="named" - else - info "Configuring self-signed certificates on host" - certType="self-signed" - fi - info "certType in postStart: $certType" - - cat <<'EOF' > defaultCertificateTemplate.json -{ - "template-name": "defaultTemplate", - "template-description": "defaultTemplate", - "key-type": "rsa", - "key-options": { - "key-length": "2048" - }, - "req": { - "version": "0", - "subject": { - "organizationName": "MarkLogic" - } - } +func (oc *OperatorContext) getScriptsForConfigMap() map[string]string { + configMapData := make(map[string]string) + logger := oc.ReqLogger + files, err := scriptsFolder.ReadDir("scripts") + if err != nil { + logger.Error(err, "Error reading scripts directory") + } + for _, file := range files { + logger.Info(file.Name()) + fileName := file.Name() + fileData, err := scriptsFolder.ReadFile("scripts/" + fileName) + if err != nil { + logger.Error(err, "Error reading file") + } + logger.Info(string(fileData)) + configMapData[fileName] = string(fileData) + } + return configMapData } -EOF - -if [[ $POD_NAME == *-0 ]] && [[ $MARKLOGIC_CLUSTER_TYPE == "bootstrap" ]]; then - log "Info: creating default certificate Template" - response=$($AUTH_CURL -X POST --header "Content-Type:application/json" -d @defaultCertificateTemplate.json http://localhost:8002/manage/v2/certificate-templates) - sleep 5s - log "Info: done creating default certificate Template" - fi - - log "Info: creating insert-host-certificates.json" - cat <<'EOF' > insert-host-certificates.json - { - "operation": "insert-host-certificates", - "certificates": [ - { - "certificate": { - "cert": "CERT", - "pkey": "PKEY" - } - } - ] - } -EOF - - log "Info: creating generateCA.xqy" - cat <<'EOF' > generateCA.xqy -xquery= - xquery version "1.0-ml"; - import module namespace pki = "http://marklogic.com/xdmp/pki" - at "/MarkLogic/pki.xqy"; - let $tid := pki:template-get-id(pki:get-template-by-name("defaultTemplate")) - return - pki:generate-template-certificate-authority($tid, 365) -EOF - - log "Info: creating createTempCert.xqy" - cat <<'EOF' > createTempCert.xqy -xquery= - xquery version "1.0-ml"; - import module namespace pki = "http://marklogic.com/xdmp/pki" - at "/MarkLogic/pki.xqy"; - import module namespace admin = "http://marklogic.com/xdmp/admin" - at "/MarkLogic/admin.xqy"; - let $tid := pki:template-get-id(pki:get-template-by-name("defaultTemplate")) - let $config := admin:get-configuration() - let $hostname := admin:host-get-name($config, admin:host-get-id($config, xdmp:host-name())) - return - pki:generate-temporary-certificate-if-necessary($tid, 365, $hostname, (), ()) -EOF - - log "Info: inserting certificates $certType" - if [[ "$certType" == "named" ]]; then - log "Info: creating named certificate" - cert_path="/run/secrets/marklogic-certs/tls.crt" - pkey_path="/run/secrets/marklogic-certs/tls.key" - cp insert-host-certificates.json insert_cert_payload.json - cert="$(<$cert_path)" - cert="${cert//$'\n'/}" - pkey="$(<$pkey_path)" - pkey="${pkey//$'\n'/}" - - sed -i "s|CERT|$cert|" insert_cert_payload.json - sed -i "s|CERTIFICATE-----|CERTIFICATE-----\\\\n|" insert_cert_payload.json - sed -i "s|-----END CERTIFICATE|\\\\n-----END CERTIFICATE|" insert_cert_payload.json - sed -i "s|PKEY|$pkey|" insert_cert_payload.json - sed -i "s|PRIVATE KEY-----|PRIVATE KEY-----\\\\n|" insert_cert_payload.json - sed -i "s|-----END RSA|\\\\n-----END RSA|" insert_cert_payload.json - sed -i "s|-----END PRIVATE|\\\\n-----END PRIVATE|" insert_cert_payload.json - - log "Info: inserting following certificates for $cert_path for $MARKLOGIC_CLUSTER_TYPE" - - if [[ $POD_NAME == *-0 ]]; then - res=$($AUTH_CURL -X POST --header "Content-Type:application/json" -d @insert_cert_payload.json http://localhost:8002/manage/v2/certificate-templates/defaultTemplate 2>&1) - else - res=$($AUTH_CURL -k -X POST --header "Content-Type:application/json" -d @insert_cert_payload.json https://localhost:8002/manage/v2/certificate-templates/defaultTemplate 2>&1) - fi - log "Info: $res" - sleep 5s - fi - if [[ $POD_NAME == *-0 ]]; then - if [[ $MARKLOGIC_CLUSTER_TYPE == "bootstrap" ]]; then - log "Info: Generating Temporary CA Certificate" - $AUTH_CURL -X POST -i -d @generateCA.xqy \ - -H "Content-type: application/x-www-form-urlencoded" \ - -H "Accept: multipart/mixed; boundary=BOUNDARY" \ - http://localhost:8000/v1/eval - resp_code=$? - info "response code for Generating Temporary CA Certificate is $resp_code" - sleep 5s - fi - - log "Info: enabling app-servers for HTTPS" - # Manage need be put in the last in the array to make sure http works for all the requests - appServers=("App-Services" "Admin" "Manage") - for appServer in ${appServers[@]}; do - log "configuring SSL for App Server $appServer" - curl --anyauth --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD \ - -X PUT -H "Content-type: application/json" -d '{"ssl-certificate-template":"defaultTemplate"}' \ - http://localhost:8002/manage/v2/servers/${appServer}/properties?group-id=${MARKLOGIC_GROUP} - sleep 5s - done - log "Info: Configure HTTPS in App Server finished" - - if [[ "$certType" == "self-signed" ]]; then - log "Info: Generate temporary certificate if necessary" - $AUTH_CURL -k -X POST -i -d @createTempCert.xqy -H "Content-type: application/x-www-form-urlencoded" \ - -H "Accept: multipart/mixed; boundary=BOUNDARY" https://localhost:8000/v1/eval - resp_code=$? - info "response code for Generate temporary certificate is $resp_code" - fi - fi - - log "Info: removing cert keys" - rm -f /run/secrets/marklogic-certs/*.key -} - -############################################################### -# Env Setup of MarkLogic -############################################################### -# Make sure username and password variables are not empty -if [[ -z "${MARKLOGIC_ADMIN_USERNAME}" ]] || [[ -z "${MARKLOGIC_ADMIN_PASSWORD}" ]]; then - error "MARKLOGIC_ADMIN_USERNAME and MARKLOGIC_ADMIN_PASSWORD must be set." exit -fi - -# generate JSON payload conditionally with license details. -if [[ -z "${LICENSE_KEY}" ]] || [[ -z "${LICENSEE}" ]]; then - LICENSE_PAYLOAD="{}" -else - info "LICENSE_KEY and LICENSEE are defined, installing MarkLogic license." - LICENSE_PAYLOAD="{\"license-key\" : \"${LICENSE_KEY}\",\"licensee\" : \"${LICENSEE}\"}" -fi - -# sets realm conditionally based on user input -if [[ -z "${REALM}" ]]; then - ML_REALM="public" -else - info "REALM is defined, setting realm." - ML_REALM="${REALM}" -fi - -if [[ -z "${MARKLOGIC_WALLET_PASSWORD}" ]]; then - MARKLOGIC_WALLET_PASSWORD_PAYLOAD="" -else - MARKLOGIC_WALLET_PASSWORD_PAYLOAD="wallet-password=${MARKLOGIC_WALLET_PASSWORD}" -fi - -############################################################### -info "Start configuring MarkLogic for $HOST_FQDN" -info "Bootstrap host: $MARKLOGIC_BOOTSTRAP_HOST" - -# Wait for current pod ready -wait_until_marklogic_ready $HOST_FQDN - -# Only do this if the bootstrap host is in the statefulset we are configuring -if [[ "${MARKLOGIC_CLUSTER_TYPE}" = "bootstrap" && "${HOST_FQDN}" = "${MARKLOGIC_BOOTSTRAP_HOST}" ]]; then - sleep 2s - init_security_db - configure_group -else - wait_bootstrap_ready - configure_group - join_cluster $HOST_FQDN -fi - -sleep 5s - -# Authentication configuration when path based is used -if [[ $POD_NAME == *-0 ]] && [[ $PATH_BASED_ROUTING == "true" ]]; then - log "Info: path based routing is set. Adapting authentication method" - resp=$(curl --anyauth -w "%{http_code}" --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s -X PUT -H "Content-type: application/json" -d '{"authentication":"basic"}' http://localhost:8002/manage/v2/servers/Admin/properties?group-id=${MARKLOGIC_GROUP}) - log "Info: Admin-Servers response code: $resp" - resp=$(curl --anyauth -w "%{http_code}" --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s -X PUT -H "Content-type: application/json" -d '{"authentication":"basic"}' http://localhost:8002/manage/v2/servers/App-Services/properties?group-id=${MARKLOGIC_GROUP}) - log "Info: App Service response code: $resp" - resp=$(curl --anyauth -w "%{http_code}" --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s -X PUT -H "Content-type: application/json" -d '{"authentication":"basic"}' http://localhost:8002/manage/v2/servers/Manage/properties?group-id=${MARKLOGIC_GROUP}) - log "Info: Manage response code: $resp" - log "Info: Default App-Servers authentication set to basic auth" -else - log "Info: This is not the boostrap host or path based routing is not set. Skipping authentication configuration" -fi -#End of authentication configuration - -if [[ $MARKLOGIC_JOIN_TLS_ENABLED == "true" ]]; then - configure_tls -fi - -info "helm script completed" -` -} diff --git a/pkg/k8sutil/scripts/copy-certs.sh b/pkg/k8sutil/scripts/copy-certs.sh new file mode 100644 index 0000000..939fee3 --- /dev/null +++ b/pkg/k8sutil/scripts/copy-certs.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" +MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/password)" +log () { + local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") + echo "${TIMESTAMP} $@" +} +if [[ -d "/tmp/server-cert-secrets" ]]; then + certType="named" +else + certType="self-signed" +fi +log "Info: [copy-certs] Proceeding with $certType certificate flow." +host_FQDN="$POD_NAME.$MARKLOGIC_FQDN_SUFFIX" +log "Info: [copy-certs] FQDN for this server: $host_FQDN" +foundMatchingCert="false" +if [[ "$certType" == "named" ]]; then + cp -f /tmp/ca-cert-secret/* /run/secrets/marklogic-certs/; + cert_paths=$(find /tmp/server-cert-secrets/tls_*.crt) + for cert_path in $cert_paths; do + cert_cn=$(openssl x509 -noout -subject -in $cert_path | sed -n 's/.*CN = \([^,]*\).*/\1/p') + log "Info: [copy-certs] FQDN for the certificate: $cert_cn" + if [[ "$host_FQDN" == "$cert_cn" ]]; then + log "Info: [copy-certs] found certificate for the server" + foundMatchingCert="true" + cp $cert_path /run/secrets/marklogic-certs/tls.crt + pkey_path=$(echo "$cert_path" | sed "s:.crt:.key:") + cp $pkey_path /run/secrets/marklogic-certs/tls.key + if [[ ! -e "$pkey_path" ]]; then + log "Error: [copy-certs] private key tls.key for certificate $cert_cn is not found. Exiting." + exit 1 + fi + + # verify the tls.crt and cacert.pem is valid, otherwise exit + openssl verify -CAfile /run/secrets/marklogic-certs/cacert.pem /run/secrets/marklogic-certs/tls.crt + if [[ $? -ne 0 ]]; then + log "Error: [copy-certs] Server certificate tls.crt verification with cacert.pem failed. Exiting." + exit 1 + fi + # verify the tls.crt and tls.key is matching, otherwise exit + privateKeyMD5=$(openssl rsa -modulus -noout -in /run/secrets/marklogic-certs/tls.key | openssl md5) + publicKeyMD5=$(openssl x509 -modulus -noout -in /run/secrets/marklogic-certs/tls.crt | openssl md5) + if [[ -z "privateKeyMD5" ]] || [[ "$privateKeyMD5" != "$publicKeyMD5" ]]; then + log "Error: [copy-certs] private key tls.key and server certificate tls.crt are not matching. Exiting." + exit 1 + fi + log "Info: [copy-certs] certificate and private key are valid." + break + fi + done + if [[ $foundMatchingCert == "false" ]]; then + if [[ $POD_NAME = *"-0" ]]; then + log "Error: [copy-certs] Failed to find matching certificate for the bootstrap server. Exiting." + exit 1 + else + log "Error: [copy-certs] Failed to find matching certificate for the non-bootstrap server. Continuing with temporary certificate for this host. Please update the certificate for this host later." + fi + fi +elif [[ "$certType" == "self-signed" ]]; then + if [[ $POD_NAME != *"-0" ]] || [[ $MARKLOGIC_CLUSTER_TYPE == "non-bootstrap" ]]; then + log "Info: [copy-certs] Getting CA for bootstrap host" + cd /run/secrets/marklogic-certs/ + echo quit | openssl s_client -showcerts -servername "${MARKLOGIC_BOOTSTRAP_HOST}" -showcerts -connect "${MARKLOGIC_BOOTSTRAP_HOST}":8000 2>&1 < /dev/null | sed -n '/-----BEGIN/,/-----END/p' > cacert.pem + fi +else + log "Error: [copy-certs] unknown certType: $certType" + exit 1 +fi \ No newline at end of file diff --git a/pkg/k8sutil/scripts/liveness-probe.sh b/pkg/k8sutil/scripts/liveness-probe.sh new file mode 100644 index 0000000..388d426 --- /dev/null +++ b/pkg/k8sutil/scripts/liveness-probe.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +log () { + local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") + # Check to make sure pod doesn't terminate if PID value is empty for any reason + if [ -n "$pid" ]; then + echo "${TIMESTAMP} $@" > /proc/$pid/fd/1 + fi +} + +pid=$(pgrep -fn start.marklogic) + +# Check if ML service is running. Exit with 1 if it is other than running +ml_status=$(/etc/init.d/MarkLogic status) + +if [[ "$ml_status" =~ "running" ]]; then + http_code=$(curl -o /tmp/probe_response.txt -s -w "%{http_code}" "http://${HOSTNAME}:8001/admin/v1/timestamp") + curl_code=$? + http_resp=$(cat /tmp/probe_response.txt) + + if [[ $curl_code -ne 0 && $http_code -ne 401 ]]; then + log "Info: [Liveness Probe] Error with MarkLogic" + log "Info: [Liveness Probe] Curl response code: "$curl_code + log "Info: [Liveness Probe] Http response code: "$http_code + log "Info: [Liveness Probe] Http response message: "$http_resp + fi + rm -f /tmp/probe_response.txt + exit 0 +else + exit 1 +fi \ No newline at end of file diff --git a/pkg/k8sutil/scripts/poststart-hook.sh b/pkg/k8sutil/scripts/poststart-hook.sh new file mode 100644 index 0000000..309a3d0 --- /dev/null +++ b/pkg/k8sutil/scripts/poststart-hook.sh @@ -0,0 +1,674 @@ +#!/bin/bash +# Refer to https://docs.marklogic.com/guide/admin-api/cluster#id_10889 for cluster joining process + +N_RETRY=60 +RETRY_INTERVAL=1 +HOST_FQDN="$(hostname).${MARKLOGIC_FQDN_SUFFIX}" + +# HTTP_PROTOCOL could be http or https +HTTP_PROTOCOL="http" +HTTPS_OPTION="" +if [[ "$MARKLOGIC_JOIN_TLS_ENABLED" == "true" ]]; then + HTTP_PROTOCOL="https" + HTTPS_OPTION="-k" +fi + +IS_BOOTSTRAP_HOST=false +if [[ "$(hostname)" == *-0 ]]; then + echo "IS_BOOTSTRAP_HOST true" + IS_BOOTSTRAP_HOST=true +else + echo "IS_BOOTSTRAP_HOST false" +fi + +MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" +MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/password)" + +pid=$(pgrep -fn start.marklogic) + +############################################################### +# Logging utility +############################################################### +info() { + log "Info" "$@" +} + +error() { + log "Error" "$1" + local EXIT_STATUS="$2" + if [[ ${EXIT_STATUS} == "exit" ]] + then + exit 1 + fi +} + +log () { + local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") + # Check to make sure pod doesn't terminate if PID value is empty for any reason + # If PID value is empty postStart hook logs are not recorded + message="${TIMESTAMP} [postStart] $@" + if [ -n "$pid" ]; then + echo $message > /proc/$pid/fd/1 + fi + + echo $message >> /tmp/script.log +} + +################################################################ +# restart_check(hostname, baseline_timestamp) +# +# Use the timestamp service to detect a server restart, given a +# a baseline timestamp. Use N_RETRY and RETRY_INTERVAL to tune +# the test length. Include authentication in the curl command +# so the function works whether or not security is initialized. +# $1 : The hostname to test against +# $2 : The baseline timestamp +# Returns 0 if restart is detected, exits with an error if not. +################################################################ +function restart_check { + local hostname=$1 + local old_timestamp=$2 + local retry_count + local last_start + + info "${hostname} - waiting for MarkLogic to restart" + + last_start=$( \ + curl -s --anyauth \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + "http://${hostname}:8001/admin/v1/timestamp" \ + ) + for ((retry_count = 0; retry_count < N_RETRY; retry_count = retry_count + 1)); do + if [ "${old_timestamp}" == "${last_start}" ] || [ -z "${last_start}" ]; then + info "${hostname} - waiting for MarkLogic to restart: ${old_timestamp} ${last_start}" + sleep ${RETRY_INTERVAL} + last_start=$( \ + curl -s --anyauth \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + "http://${hostname}:8001/admin/v1/timestamp" \ + ) + else + info "${hostname} - MarkLogic has restarted" + return 0 + fi + done + error "${hostname} - failed to restart" exit +} +################################################################ +# retry_and_timeout(target_url, expected_response_code, additional_options, return_error) +# The third argument is optional and can be used to pass additional options to curl. +# Fourth argurment is optional, default is set to true, can be used when custom error handling is required, +# if set to true means function will return error and exit if curl fails N_RETRY times +# setting to false means function will return response code instead of failing and exiting. +# Retry a curl command until it returns the expected response +# code or fails N_RETRY times. +# Use RETRY_INTERVAL to tune the test length. +# Validate that response code is the same as expected response +# code or exit with an error. +# +# $1 : The target url to test against +# $2 : The expected response code +# $3 : Additional options to pass to curl +# $4 : Option to return error or response code in case of error +################################################################ +function curl_retry_validate { + local retry_count + local return_error="${4:-true}" + for ((retry_count = 0; retry_count < N_RETRY; retry_count = retry_count + 1)); do + request="curl -m 30 -s -w '%{http_code}' $3 $1" + response_code=$(eval "${request}") + if [[ ${response_code} -eq $2 ]]; then + return "${response_code}" + fi + sleep ${RETRY_INTERVAL} + done + if [[ "${return_error}" = "false" ]] ; then + return "${response_code}" + fi + error "Expected response code ${2}, got ${response_code} from ${1}." exit +} + +################################################################ +# Function to initialize a host +# $1: The host name +# return values: 0 - successfully initialized +# 1 - host not reachable +################################################################ +function wait_until_marklogic_ready { + local host=$1 + info "wait until $host is ready" + timestamp=$( curl -s --anyauth \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + http://${host}:8001/admin/v1/timestamp ) + if [ -z "${timestamp}" ]; then + info "${host} - not responding yet" + sleep 5s + wait_until_marklogic_ready $host + else + info "${host} - responding, calling init" + out="/tmp/${host}.out" + + response_code=$( \ + curl --anyauth -m 30 -s --retry 5 \ + -w '%{http_code}' -o "${out}" \ + -i -X POST -H "Content-type:application/json" \ + -d "${LICENSE_PAYLOAD}" \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + http://${host}:8001/admin/v1/init \ + ) + if [ "${response_code}" = "202" ]; then + info "${host} - init called, restart triggered" + last_startup=$( \ + cat "${out}" | + grep "last-startup" | + sed 's%^.*\(.*\).*$%\1%' \ + ) + + restart_check "${host}" "${last_startup}" + info "${host} - restarted" + info "${host} - init complete" + elif [ "${response_code}" -eq "204" ]; then + info "${host} - init called, no restart triggered" + info "${host} - init complete" + else + info "${host} - error calling init: ${response_code}" + fi + fi +} + +################################################################ +# Function to initialize a host +# $1: The host name +# return values: 0 - successfully initialized +# 1 - host not reachable +################################################################ +function init_marklogic_host { + local hostname=$1 + info "initializing host: $hostname" + timestamp=$( \ + curl -s --anyauth \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + http://${hostname}:8001/admin/v1/timestamp \ + ) + if [ -z "${timestamp}" ]; then + info "${hostname} - not responding yet" + return 1 + fi + info "${hostname} - responding, calling init" + output_path="/tmp/${hostname}.out" + response_code=$( \ + curl --anyauth -m 30 -s --retry 5 \ + -w '%{http_code}' -o "${output_path}" \ + -i -X POST -H "Content-type:application/json" \ + -d "${LICENSE_PAYLOAD}" \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + http://${hostname}:8001/admin/v1/init \ + ) + + if [ "${response_code}" = "202" ]; then + info "${hostname} - init called, restart triggered" + last_startup=$( \ + cat "${output_path}" | + grep "last-startup" | + sed 's%^.*\(.*\).*$%\1%' \ + ) + + restart_check "${hostname}" "${last_startup}" + return 0 + elif [ "${response_code}" -eq "204" ]; then + info "${hostname} - init called, no restart triggered" + info "${hostname} - init complete" + return 0 + else + info "${hostname} - error calling init: ${response_code}" + [ -f "${out}" ] && cat "${out}" + fi +} + +################################################################ +# Function to bootstrap host is ready: +# 1. If TLS is not enabled, wait until Security DB is installed. +# 2. If TLS is enabled, wait until TLS is turned on in App Server +# return values: 0 - admin user successfully initialized +################################################################ +function wait_bootstrap_ready { + resp=$(curl -w '%{http_code}' -o /dev/null http://$MARKLOGIC_BOOTSTRAP_HOST:8001/admin/v1/timestamp ) + if [[ "$MARKLOGIC_JOIN_TLS_ENABLED" == "true" ]]; then + # return 403 if tls is enabled + if [[ $resp -eq 403 ]]; then + info "Bootstrap host is ready with TLS enabled" + else + info "Timestamp response code:$resp. Bootstrap host is not ready with TLS enabled, try again in 10s" + sleep 10s + wait_bootstrap_ready + fi + else + if [[ $resp -eq 401 ]]; then + info "Bootstrap host is ready with no TLS" + else + info "Timestamp response code:$resp. Bootstrap host is not ready, try again in 10s" + sleep 10s + wait_bootstrap_ready + fi + fi +} + +################################################################ +# Function to initialize admin user and security DB +# +# return values: 0 - admin user successfully initialized +################################################################ +function init_security_db { + info "initializing as bootstrap cluster" + + # check to see if the bootstrap host is already configured + response_code=$( \ + curl -s --anyauth \ + -w '%{http_code}' -o "/tmp/${MARKLOGIC_BOOTSTRAP_HOST}.out" \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" $HTTPS_OPTION \ + $HTTP_PROTOCOL://$MARKLOGIC_BOOTSTRAP_HOST:8002/manage/v2/hosts/$MARKLOGIC_BOOTSTRAP_HOST/properties + ) + + if [ "${response_code}" = "200" ]; then + info "${MARKLOGIC_BOOTSTRAP_HOST} - bootstrap security already initialized" + return 0 + else + info "${MARKLOGIC_BOOTSTRAP_HOST} - initializing bootstrap security" + + # Get last restart timestamp directly before instance-admin call to verify restart after + timestamp=$( \ + curl -s --anyauth \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + "http://${MARKLOGIC_BOOTSTRAP_HOST}:8001/admin/v1/timestamp" \ + ) + + curl_retry_validate "http://${MARKLOGIC_BOOTSTRAP_HOST}:8001/admin/v1/instance-admin" 202 \ + "-o /dev/null \ + -X POST -H \"Content-type:application/x-www-form-urlencoded; charset=utf-8\" \ + -d \"admin-username=${MARKLOGIC_ADMIN_USERNAME}\" --data-urlencode \"admin-password=${MARKLOGIC_ADMIN_PASSWORD}\" \ + -d \"realm=${ML_REALM}\" -d \"${MARKLOGIC_WALLET_PASSWORD_PAYLOAD}\"" + + restart_check "${MARKLOGIC_BOOTSTRAP_HOST}" "${timestamp}" + + info "${MARKLOGIC_BOOTSTRAP_HOST} - bootstrap security initialized" + return 0 + fi +} + +################################################################ +# Function to join marklogic host to cluster +# +# return values: 0 - admin user successfully initialized +################################################################ +function join_cluster { + hostname=$1 + + # check if Bootstrap Host is ready + # if server could not be reached, response_code == 000 + # if host has not join cluster, return 404 + # if bootstrap host not init, return 403 + # if Security DB not set or credential not correct return 401 + # if host is already in cluster, return 200 + response_code=$( curl -s --anyauth -o /dev/null -w '%{http_code}' \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" $HTTPS_OPTION \ + $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/hosts/${hostname}/properties?format=xml \ + ) + + info "response_code: $response_code" + + if [ "${response_code}" = "200" ]; then + info "host has already joined the cluster" + return 0 + elif [ "${response_code}" != "404" ]; then + sleep 10s + join_cluster $hostname + else + info "Proceed to joining bootstrap host" + fi + + # process to join the host + + # Wait until the group is ready + retry_count=10 + while [ $retry_count -gt 0 ]; do + GROUP_RESP_CODE=$( curl --anyauth -m 20 -s -o /dev/null -w "%{http_code}" $HTTPS_OPTION -X GET $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups/${MARKLOGIC_GROUP} --anyauth --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} ) + if [[ ${GROUP_RESP_CODE} -eq 200 ]]; then + info "Found the group, process to join the group" + break + else + ((retry_count--)) + info "GROUP_RESP_CODE: $GROUP_RESP_CODE , retry $retry_count times to joining ${MARKLOGIC_GROUP} group in marklogic cluster" + sleep 10s + fi + done + + if [[ $retry_count -le 0 ]]; then + info "retry_count: $retry_count" + error "pass timeout to wait for the group ready" + exit 1 + fi + + info "${hostname} - joining group ${MARKLOGIC_GROUP}" + payload=\"group=${MARKLOGIC_GROUP}\" + curl_retry_validate "http://${hostname}:8001/admin/v1/server-config" 200 \ + "--anyauth --user \"${MARKLOGIC_ADMIN_USERNAME}\":\"${MARKLOGIC_ADMIN_PASSWORD}\" \ + -o /tmp/${hostname}.xml -X GET -H \"Accept: application/xml\"" + + curl_retry_validate "$HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8001/admin/v1/cluster-config" 200 \ + "--anyauth $HTTPS_OPTION --user \"${MARKLOGIC_ADMIN_USERNAME}\":\"${MARKLOGIC_ADMIN_PASSWORD}\" \ + -X POST -d \"${payload}\" \ + --data-urlencode \"server-config@/tmp/${hostname}.xml\" \ + -H \"Content-type: application/x-www-form-urlencoded\" \ + -o /tmp/${hostname}_cluster.zip" + + timestamp=$( \ + curl -s --anyauth $HTTPS_OPTION \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + "http://${hostname}:8001/admin/v1/timestamp" \ + ) + + curl_retry_validate "http://${hostname}:8001/admin/v1/cluster-config" 202 \ + "-o /dev/null --anyauth --user \"${MARKLOGIC_ADMIN_USERNAME}\":\"${MARKLOGIC_ADMIN_PASSWORD}\" \ + -X POST -H \"Content-type: application/zip\" \ + --data-binary @/tmp/${hostname}_cluster.zip" + + # 202 causes restart + info "${hostname} - restart triggered" + # restart_check "${hostname}" "${timestamp}" + + info "${hostname} - joined group ${MARKLOGIC_GROUP}" +} + +################################################################ +# Function to configure MarkLogic Group +# +# return +################################################################ +function configure_group { + + if [[ "$IS_BOOTSTRAP_HOST" == "true" ]]; then + group_cfg_template='{"group-name":"%s", "xdqp-ssl-enabled":"%s"}' + group_cfg=$(printf "$group_cfg_template" "$MARKLOGIC_GROUP" "$XDQP_SSL_ENABLED") + + # check if host is already in and get the current cluster + response_code=$( \ + curl -s --anyauth \ + -w '%{http_code}' -o "/tmp/groups.out" \ + --user "${MARKLOGIC_ADMIN_USERNAME}":"${MARKLOGIC_ADMIN_PASSWORD}" \ + http://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/hosts/${HOST_FQDN}/properties?format=xml + ) + + if [ "${response_code}" = "200" ]; then + current_group=$( \ + cat "/tmp/groups.out" | + grep "group" | + sed 's%^.*\(.*\).*$%\1%' \ + ) + + info "current_group: $current_group" + info "group_cfg: $group_cfg" + + # curl retry doesn't work in the lower version + response_code=$( \ + curl -s --anyauth \ + --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} \ + -w '%{http_code}' \ + -X PUT \ + -H "Content-type: application/json" \ + -d "${group_cfg}" \ + http://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups/${current_group}/properties \ + ) + + info "response_code: $response_code" + + if [[ "${response_code}" = "204" ]]; then + info "group \"${current_group}\" updated" + elif [[ "${response_code}" = "202" ]]; then + # Note: THIS SHOULD NOT HAPPEN WITH THE CURRENT GROUP CONFIG + info "group \"${current_group}\" updated and a restart of all hosts in the group was triggered" + else + info "unexpected response when updating group \"${current_group}\": ${response_code}" + fi + + fi + + if [[ "$MARKLOGIC_CLUSTER_TYPE" == "non-bootstrap" ]]; then + info "creating group for other Helm Chart" + + # Create a group if group is not already exits + GROUP_RESP_CODE=$( curl --anyauth -m 20 -s -o /dev/null -w "%{http_code}" $HTTPS_OPTION -X GET $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups/${MARKLOGIC_GROUP} --anyauth --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} ) + if [[ ${GROUP_RESP_CODE} -eq 200 ]]; then + info "Skipping creation of group $MARKLOGIC_GROUP as it already exists on the MarkLogic cluster." + else + res_code=$(curl --anyauth --user ${MARKLOGIC_ADMIN_USERNAME}:${MARKLOGIC_ADMIN_PASSWORD} $HTTPS_OPTION -m 20 -s -w '%{http_code}' -X POST -d "${group_cfg}" -H "Content-type: application/json" $HTTP_PROTOCOL://${MARKLOGIC_BOOTSTRAP_HOST}:8002/manage/v2/groups) + if [[ ${res_code} -eq 201 ]]; then + log "Info: [initContainer] Successfully configured group $MARKLOGIC_GROUP on the MarkLogic cluster." + else + log "Info: [initContainer] Expected response code 201, got $res_code" + fi + fi + + fi + else + info "not bootstrap host. Skip group configuration" + fi + +} + +function configure_tls { + info "Configuring TLS for App Servers" + + AUTH_CURL="curl --anyauth --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s " + + cd /tmp/ + if [[ -e "/run/secrets/marklogic-certs/tls.crt" ]]; then + info "Configuring named certificates on host" + certType="named" + else + info "Configuring self-signed certificates on host" + certType="self-signed" + fi + info "certType in postStart: $certType" + + cat <<'EOF' > defaultCertificateTemplate.json +{ + "template-name": "defaultTemplate", + "template-description": "defaultTemplate", + "key-type": "rsa", + "key-options": { + "key-length": "2048" + }, + "req": { + "version": "0", + "subject": { + "organizationName": "MarkLogic" + } + } +} +EOF + +if [[ $POD_NAME == *-0 ]] && [[ $MARKLOGIC_CLUSTER_TYPE == "bootstrap" ]]; then + log "Info: creating default certificate Template" + response=$($AUTH_CURL -X POST --header "Content-Type:application/json" -d @defaultCertificateTemplate.json http://localhost:8002/manage/v2/certificate-templates) + sleep 5s + log "Info: done creating default certificate Template" + fi + + log "Info: creating insert-host-certificates.json" + cat <<'EOF' > insert-host-certificates.json + { + "operation": "insert-host-certificates", + "certificates": [ + { + "certificate": { + "cert": "CERT", + "pkey": "PKEY" + } + } + ] + } +EOF + + log "Info: creating generateCA.xqy" + cat <<'EOF' > generateCA.xqy +xquery= + xquery version "1.0-ml"; + import module namespace pki = "http://marklogic.com/xdmp/pki" + at "/MarkLogic/pki.xqy"; + let $tid := pki:template-get-id(pki:get-template-by-name("defaultTemplate")) + return + pki:generate-template-certificate-authority($tid, 365) +EOF + + log "Info: creating createTempCert.xqy" + cat <<'EOF' > createTempCert.xqy +xquery= + xquery version "1.0-ml"; + import module namespace pki = "http://marklogic.com/xdmp/pki" + at "/MarkLogic/pki.xqy"; + import module namespace admin = "http://marklogic.com/xdmp/admin" + at "/MarkLogic/admin.xqy"; + let $tid := pki:template-get-id(pki:get-template-by-name("defaultTemplate")) + let $config := admin:get-configuration() + let $hostname := admin:host-get-name($config, admin:host-get-id($config, xdmp:host-name())) + return + pki:generate-temporary-certificate-if-necessary($tid, 365, $hostname, (), ()) +EOF + + log "Info: inserting certificates $certType" + if [[ "$certType" == "named" ]]; then + log "Info: creating named certificate" + cert_path="/run/secrets/marklogic-certs/tls.crt" + pkey_path="/run/secrets/marklogic-certs/tls.key" + cp insert-host-certificates.json insert_cert_payload.json + cert="$(<$cert_path)" + cert="${cert//$'\n'/}" + pkey="$(<$pkey_path)" + pkey="${pkey//$'\n'/}" + + sed -i "s|CERT|$cert|" insert_cert_payload.json + sed -i "s|CERTIFICATE-----|CERTIFICATE-----\\\\n|" insert_cert_payload.json + sed -i "s|-----END CERTIFICATE|\\\\n-----END CERTIFICATE|" insert_cert_payload.json + sed -i "s|PKEY|$pkey|" insert_cert_payload.json + sed -i "s|PRIVATE KEY-----|PRIVATE KEY-----\\\\n|" insert_cert_payload.json + sed -i "s|-----END RSA|\\\\n-----END RSA|" insert_cert_payload.json + sed -i "s|-----END PRIVATE|\\\\n-----END PRIVATE|" insert_cert_payload.json + + log "Info: inserting following certificates for $cert_path for $MARKLOGIC_CLUSTER_TYPE" + + if [[ $POD_NAME == *-0 ]]; then + res=$($AUTH_CURL -X POST --header "Content-Type:application/json" -d @insert_cert_payload.json http://localhost:8002/manage/v2/certificate-templates/defaultTemplate 2>&1) + else + res=$($AUTH_CURL -k -X POST --header "Content-Type:application/json" -d @insert_cert_payload.json https://localhost:8002/manage/v2/certificate-templates/defaultTemplate 2>&1) + fi + log "Info: $res" + sleep 5s + fi + + if [[ $POD_NAME == *-0 ]]; then + if [[ $MARKLOGIC_CLUSTER_TYPE == "bootstrap" ]]; then + log "Info: Generating Temporary CA Certificate" + $AUTH_CURL -X POST -i -d @generateCA.xqy \ + -H "Content-type: application/x-www-form-urlencoded" \ + -H "Accept: multipart/mixed; boundary=BOUNDARY" \ + http://localhost:8000/v1/eval + resp_code=$? + info "response code for Generating Temporary CA Certificate is $resp_code" + sleep 5s + fi + + log "Info: enabling app-servers for HTTPS" + # Manage need be put in the last in the array to make sure http works for all the requests + appServers=("App-Services" "Admin" "Manage") + for appServer in ${appServers[@]}; do + log "configuring SSL for App Server $appServer" + curl --anyauth --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD \ + -X PUT -H "Content-type: application/json" -d '{"ssl-certificate-template":"defaultTemplate"}' \ + http://localhost:8002/manage/v2/servers/${appServer}/properties?group-id=${MARKLOGIC_GROUP} + sleep 5s + done + log "Info: Configure HTTPS in App Server finished" + + if [[ "$certType" == "self-signed" ]]; then + log "Info: Generate temporary certificate if necessary" + $AUTH_CURL -k -X POST -i -d @createTempCert.xqy -H "Content-type: application/x-www-form-urlencoded" \ + -H "Accept: multipart/mixed; boundary=BOUNDARY" https://localhost:8000/v1/eval + resp_code=$? + info "response code for Generate temporary certificate is $resp_code" + fi + fi + + log "Info: removing cert keys" + rm -f /run/secrets/marklogic-certs/*.key +} + +############################################################### +# Env Setup of MarkLogic +############################################################### +# Make sure username and password variables are not empty +if [[ -z "${MARKLOGIC_ADMIN_USERNAME}" ]] || [[ -z "${MARKLOGIC_ADMIN_PASSWORD}" ]]; then + error "MARKLOGIC_ADMIN_USERNAME and MARKLOGIC_ADMIN_PASSWORD must be set." exit +fi + +# generate JSON payload conditionally with license details. +if [[ -z "${LICENSE_KEY}" ]] || [[ -z "${LICENSEE}" ]]; then + LICENSE_PAYLOAD="{}" +else + info "LICENSE_KEY and LICENSEE are defined, installing MarkLogic license." + LICENSE_PAYLOAD="{\"license-key\" : \"${LICENSE_KEY}\",\"licensee\" : \"${LICENSEE}\"}" +fi + +# sets realm conditionally based on user input +if [[ -z "${REALM}" ]]; then + ML_REALM="public" +else + info "REALM is defined, setting realm." + ML_REALM="${REALM}" +fi + +if [[ -z "${MARKLOGIC_WALLET_PASSWORD}" ]]; then + MARKLOGIC_WALLET_PASSWORD_PAYLOAD="" +else + MARKLOGIC_WALLET_PASSWORD_PAYLOAD="wallet-password=${MARKLOGIC_WALLET_PASSWORD}" +fi + +############################################################### +info "Start configuring MarkLogic for $HOST_FQDN" +info "Bootstrap host: $MARKLOGIC_BOOTSTRAP_HOST" + +# Wait for current pod ready +wait_until_marklogic_ready $HOST_FQDN + +# Only do this if the bootstrap host is in the statefulset we are configuring +if [[ "${MARKLOGIC_CLUSTER_TYPE}" = "bootstrap" && "${HOST_FQDN}" = "${MARKLOGIC_BOOTSTRAP_HOST}" ]]; then + sleep 2s + init_security_db + configure_group +else + wait_bootstrap_ready + configure_group + join_cluster $HOST_FQDN +fi + +sleep 5s + +# Authentication configuration when path based is used +if [[ $POD_NAME == *-0 ]] && [[ $PATH_BASED_ROUTING == "true" ]]; then + log "Info: path based routing is set. Adapting authentication method" + resp=$(curl --anyauth -w "%{http_code}" --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s -X PUT -H "Content-type: application/json" -d '{"authentication":"basic"}' http://localhost:8002/manage/v2/servers/Admin/properties?group-id=${MARKLOGIC_GROUP}) + log "Info: Admin-Servers response code: $resp" + resp=$(curl --anyauth -w "%{http_code}" --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s -X PUT -H "Content-type: application/json" -d '{"authentication":"basic"}' http://localhost:8002/manage/v2/servers/App-Services/properties?group-id=${MARKLOGIC_GROUP}) + log "Info: App Service response code: $resp" + resp=$(curl --anyauth -w "%{http_code}" --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD -m 20 -s -X PUT -H "Content-type: application/json" -d '{"authentication":"basic"}' http://localhost:8002/manage/v2/servers/Manage/properties?group-id=${MARKLOGIC_GROUP}) + log "Info: Manage response code: $resp" + log "Info: Default App-Servers authentication set to basic auth" +else + log "Info: This is not the boostrap host or path based routing is not set. Skipping authentication configuration" +fi +#End of authentication configuration + +if [[ $MARKLOGIC_JOIN_TLS_ENABLED == "true" ]]; then + configure_tls +fi + +info "helm script completed" \ No newline at end of file diff --git a/pkg/k8sutil/scripts/prestop-hook.sh b/pkg/k8sutil/scripts/prestop-hook.sh new file mode 100644 index 0000000..67220fd --- /dev/null +++ b/pkg/k8sutil/scripts/prestop-hook.sh @@ -0,0 +1,54 @@ +#!/bin/bash + +MARKLOGIC_ADMIN_USERNAME="$(< /run/secrets/ml-secrets/username)" +MARKLOGIC_ADMIN_PASSWORD="$(< /run/secrets/ml-secrets/password)" + +log () { + local TIMESTAMP=$(date +"%Y-%m-%d %T.%3N") + # Check to make sure pod doesn't terminate if PID value is empty for any reason + # If PID value is empty preStart hook logs are not recorded + if [ -n "$pid" ]; then + echo "${TIMESTAMP} $@" > /proc/$pid/fd/1 + fi +} + +pid=$(pgrep -fn start.marklogic) +log "Info: [prestop] Prestop Hook Execution" + +my_host=$(hostname -f) + +HTTP_PROTOCOL="http" +HTTPS_OPTION="" +if [[ "$MARKLOGIC_JOIN_TLS_ENABLED" == "true" ]]; then + HTTP_PROTOCOL="https" + HTTPS_OPTION="-k" +fi +log "Info: [prestop] MarkLogic Pod Hostname: "$my_host +for ((i = 0; i < 5; i = i + 1)); do + res_code=$(curl --anyauth --user $MARKLOGIC_ADMIN_USERNAME:$MARKLOGIC_ADMIN_PASSWORD \ + -o /dev/null -m 10 -s -w %{http_code} \ + -i -X POST ${HTTPS_OPTION} --data "state=shutdown&failover=true" \ + -H "Content-type: application/x-www-form-urlencoded" \ + ${HTTP_PROTOCOL}://localhost:8002/manage/v2/hosts/$my_host?format=json) + + if [[ ${res_code} -eq 202 ]]; then + log "Info: [prestop] Host shut down response code: "$res_code + + while (true) + do + ml_status=$(service MarkLogic status) + log "Info: [prestop] MarkLogic Status: "$ml_status + if [[ "$ml_status" =~ "running" ]]; then + sleep 5s + continue + else + break + fi + done + break + else + log "ERROR: [prestop] Retry Attempt: "$i + log "ERROR: [prestop] Host shut down expected response code 202, got "$res_code + sleep 10s + fi +done \ No newline at end of file From 17d1c6aa4c2434b88dfc2f38af9405614de433aa Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Wed, 5 Jun 2024 23:59:27 -0700 Subject: [PATCH 08/12] reconcile probes --- api/v1alpha1/common_types.go | 18 +- api/v1alpha1/marklogicgroup_types.go | 7 +- api/v1alpha1/zz_generated.deepcopy.go | 47 +- ...abase.marklogic.com_marklogicclusters.yaml | 454 ++---------------- ...atabase.marklogic.com_marklogicgroups.yaml | 424 ++-------------- config/samples/marklogicgroup.yaml | 16 +- pkg/k8sutil/configmap.go | 2 - pkg/k8sutil/statefulset.go | 81 ++-- 8 files changed, 135 insertions(+), 914 deletions(-) diff --git a/api/v1alpha1/common_types.go b/api/v1alpha1/common_types.go index 860dd31..91dbca7 100644 --- a/api/v1alpha1/common_types.go +++ b/api/v1alpha1/common_types.go @@ -4,21 +4,17 @@ import ( corev1 "k8s.io/api/core/v1" ) -type Probe struct { - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:default=1 +type ContainerProbe struct { + Enabled bool `json:"enabled,omitempty"` + // +kubebuilder:validation:Minimum=0 InitialDelaySeconds int32 `json:"initialDelaySeconds,omitempty"` - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:default=1 + // +kubebuilder:validation:Minimum=0 TimeoutSeconds int32 `json:"timeoutSeconds,omitempty"` - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:default=10 + // +kubebuilder:validation:Minimum=0 PeriodSeconds int32 `json:"periodSeconds,omitempty"` - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:default=1 + // +kubebuilder:validation:Minimum=0 SuccessThreshold int32 `json:"successThreshold,omitempty"` - // +kubebuilder:validation:Minimum=1 - // +kubebuilder:default=3 + // +kubebuilder:validation:Minimum=0 FailureThreshold int32 `json:"failureThreshold,omitempty"` } diff --git a/api/v1alpha1/marklogicgroup_types.go b/api/v1alpha1/marklogicgroup_types.go index c006eb7..e2f46a5 100644 --- a/api/v1alpha1/marklogicgroup_types.go +++ b/api/v1alpha1/marklogicgroup_types.go @@ -56,9 +56,10 @@ type MarklogicGroupSpec struct { TopologySpreadConstraints []corev1.TopologySpreadConstraint `json:"topologySpreadConstraints,omitempty"` PriorityClassName string `json:"priorityClassName,omitempty"` - LivenessProbe *corev1.Probe `json:"livenessProbe,omitempty"` - ReadinessProbe *corev1.Probe `json:"readinessProbe,omitempty"` - StartupProbe *corev1.Probe `json:"startupProbe,omitempty"` + // +kubebuilder:default:={enabled: true, initialDelaySeconds: 30, timeoutSeconds: 5, periodSeconds: 30, successThreshold: 1, failureThreshold: 3} + LivenessProbe ContainerProbe `json:"livenessProbe,omitempty"` + // +kubebuilder:default:={enabled: false, initialDelaySeconds: 10, timeoutSeconds: 5, periodSeconds: 30, successThreshold: 1, failureThreshold: 3} + ReadinessProbe ContainerProbe `json:"readinessProbe,omitempty"` GroupConfig GroupConfig `json:"groupConfigs,omitempty"` License *License `json:"license,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 745b3e7..8723601 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -57,6 +57,21 @@ func (in *AdminAuth) DeepCopy() *AdminAuth { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ContainerProbe) DeepCopyInto(out *ContainerProbe) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ContainerProbe. +func (in *ContainerProbe) DeepCopy() *ContainerProbe { + if in == nil { + return nil + } + out := new(ContainerProbe) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GroupConfig) DeepCopyInto(out *GroupConfig) { *out = *in @@ -320,21 +335,8 @@ func (in *MarklogicGroupSpec) DeepCopyInto(out *MarklogicGroupSpec) { (*in)[i].DeepCopyInto(&(*out)[i]) } } - if in.LivenessProbe != nil { - in, out := &in.LivenessProbe, &out.LivenessProbe - *out = new(v1.Probe) - (*in).DeepCopyInto(*out) - } - if in.ReadinessProbe != nil { - in, out := &in.ReadinessProbe, &out.ReadinessProbe - *out = new(v1.Probe) - (*in).DeepCopyInto(*out) - } - if in.StartupProbe != nil { - in, out := &in.StartupProbe, &out.StartupProbe - *out = new(v1.Probe) - (*in).DeepCopyInto(*out) - } + out.LivenessProbe = in.LivenessProbe + out.ReadinessProbe = in.ReadinessProbe out.GroupConfig = in.GroupConfig if in.License != nil { in, out := &in.License, &out.License @@ -405,21 +407,6 @@ func (in *MarklogicGroups) DeepCopy() *MarklogicGroups { return out } -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Probe) DeepCopyInto(out *Probe) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Probe. -func (in *Probe) DeepCopy() *Probe { - if in == nil { - return nil - } - out := new(Probe) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Storage) DeepCopyInto(out *Storage) { *out = *in diff --git a/config/crd/bases/database.marklogic.com_marklogicclusters.yaml b/config/crd/bases/database.marklogic.com_marklogicclusters.yaml index 466cd2a..502f064 100644 --- a/config/crd/bases/database.marklogic.com_marklogicclusters.yaml +++ b/config/crd/bases/database.marklogic.com_marklogicclusters.yaml @@ -1200,157 +1200,35 @@ spec: type: string type: object livenessProbe: - description: Probe describes a health check to be performed - against a container to determine whether it is alive or - ready to receive traffic. + default: + enabled: true + failureThreshold: 3 + initialDelaySeconds: 30 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 properties: - exec: - description: Exec specifies the action to take. - properties: - command: - description: Command is the command line to execute - inside the container, the working directory for - the command is root ('/') in the container's - filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions - ('|', etc) won't work. To use a shell, you need - to explicitly call out to that shell. Exit status - of 0 is treated as live/healthy and non-zero is - unhealthy. - items: - type: string - type: array - type: object + enabled: + type: boolean failureThreshold: - description: Minimum consecutive failures for the probe - to be considered failed after having succeeded. Defaults - to 3. Minimum value is 1. format: int32 + minimum: 0 type: integer - grpc: - description: GRPC specifies an action involving a GRPC - port. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - description: "Service is the name of the service - to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - \n If this is not specified, the default behavior - is defined by gRPC." - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies the http request to perform. - properties: - host: - description: Host name to connect to, defaults to - the pod IP. You probably want to set "Host" in - httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. - HTTP allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: The header field name. This will - be canonicalized upon output, so case-variant - names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: Name or number of the port to access - on the container. Number must be in the range - 1 to 65535. Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: Scheme to use for connecting to the - host. Defaults to HTTP. - type: string - required: - - port - type: object initialDelaySeconds: - description: 'Number of seconds after the container - has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' format: int32 + minimum: 0 type: integer periodSeconds: - description: How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. format: int32 + minimum: 0 type: integer successThreshold: - description: Minimum consecutive successes for the probe - to be considered successful after having failed. Defaults - to 1. Must be 1 for liveness and startup. Minimum - value is 1. format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies an action involving - a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, - defaults to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: Number or name of the port to access - on the container. Number must be in the range - 1 to 65535. Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: Optional duration in seconds the pod needs - to terminate gracefully upon probe failure. The grace - period is the duration in seconds after the processes - running in the pod are sent a termination signal and - the time when the processes are forcibly halted with - a kill signal. Set this value longer than the expected - cleanup time for your process. If this value is nil, - the pod's terminationGracePeriodSeconds will be used. - Otherwise, this value overrides the value provided - by the pod spec. Value must be non-negative integer. - The value zero indicates stop immediately via the - kill signal (no opportunity to shut down). This is - a beta field and requires enabling ProbeTerminationGracePeriod - feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds - is used if unset. - format: int64 + minimum: 0 type: integer timeoutSeconds: - description: 'Number of seconds after which the probe - times out. Defaults to 1 second. Minimum value is - 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' format: int32 + minimum: 0 type: integer type: object name: @@ -1972,157 +1850,35 @@ spec: priorityClassName: type: string readinessProbe: - description: Probe describes a health check to be performed - against a container to determine whether it is alive or - ready to receive traffic. + default: + enabled: false + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 properties: - exec: - description: Exec specifies the action to take. - properties: - command: - description: Command is the command line to execute - inside the container, the working directory for - the command is root ('/') in the container's - filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions - ('|', etc) won't work. To use a shell, you need - to explicitly call out to that shell. Exit status - of 0 is treated as live/healthy and non-zero is - unhealthy. - items: - type: string - type: array - type: object + enabled: + type: boolean failureThreshold: - description: Minimum consecutive failures for the probe - to be considered failed after having succeeded. Defaults - to 3. Minimum value is 1. format: int32 + minimum: 0 type: integer - grpc: - description: GRPC specifies an action involving a GRPC - port. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - description: "Service is the name of the service - to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - \n If this is not specified, the default behavior - is defined by gRPC." - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies the http request to perform. - properties: - host: - description: Host name to connect to, defaults to - the pod IP. You probably want to set "Host" in - httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. - HTTP allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: The header field name. This will - be canonicalized upon output, so case-variant - names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: Name or number of the port to access - on the container. Number must be in the range - 1 to 65535. Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: Scheme to use for connecting to the - host. Defaults to HTTP. - type: string - required: - - port - type: object initialDelaySeconds: - description: 'Number of seconds after the container - has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' format: int32 + minimum: 0 type: integer periodSeconds: - description: How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. format: int32 + minimum: 0 type: integer successThreshold: - description: Minimum consecutive successes for the probe - to be considered successful after having failed. Defaults - to 1. Must be 1 for liveness and startup. Minimum - value is 1. format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies an action involving - a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, - defaults to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: Number or name of the port to access - on the container. Number must be in the range - 1 to 65535. Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: Optional duration in seconds the pod needs - to terminate gracefully upon probe failure. The grace - period is the duration in seconds after the processes - running in the pod are sent a termination signal and - the time when the processes are forcibly halted with - a kill signal. Set this value longer than the expected - cleanup time for your process. If this value is nil, - the pod's terminationGracePeriodSeconds will be used. - Otherwise, this value overrides the value provided - by the pod spec. Value must be non-negative integer. - The value zero indicates stop immediately via the - kill signal (no opportunity to shut down). This is - a beta field and requires enabling ProbeTerminationGracePeriod - feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds - is used if unset. - format: int64 + minimum: 0 type: integer timeoutSeconds: - description: 'Number of seconds after which the probe - times out. Defaults to 1 second. Minimum value is - 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' format: int32 + minimum: 0 type: integer type: object replicas: @@ -2365,160 +2121,6 @@ spec: type: string type: object type: object - startupProbe: - description: Probe describes a health check to be performed - against a container to determine whether it is alive or - ready to receive traffic. - properties: - exec: - description: Exec specifies the action to take. - properties: - command: - description: Command is the command line to execute - inside the container, the working directory for - the command is root ('/') in the container's - filesystem. The command is simply exec'd, it is - not run inside a shell, so traditional shell instructions - ('|', etc) won't work. To use a shell, you need - to explicitly call out to that shell. Exit status - of 0 is treated as live/healthy and non-zero is - unhealthy. - items: - type: string - type: array - type: object - failureThreshold: - description: Minimum consecutive failures for the probe - to be considered failed after having succeeded. Defaults - to 3. Minimum value is 1. - format: int32 - type: integer - grpc: - description: GRPC specifies an action involving a GRPC - port. - properties: - port: - description: Port number of the gRPC service. Number - must be in the range 1 to 65535. - format: int32 - type: integer - service: - description: "Service is the name of the service - to place in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - \n If this is not specified, the default behavior - is defined by gRPC." - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies the http request to perform. - properties: - host: - description: Host name to connect to, defaults to - the pod IP. You probably want to set "Host" in - httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. - HTTP allows repeated headers. - items: - description: HTTPHeader describes a custom header - to be used in HTTP probes - properties: - name: - description: The header field name. This will - be canonicalized upon output, so case-variant - names will be understood as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: Name or number of the port to access - on the container. Number must be in the range - 1 to 65535. Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: Scheme to use for connecting to the - host. Defaults to HTTP. - type: string - required: - - port - type: object - initialDelaySeconds: - description: 'Number of seconds after the container - has started before liveness probes are initiated. - More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' - format: int32 - type: integer - periodSeconds: - description: How often (in seconds) to perform the probe. - Default to 10 seconds. Minimum value is 1. - format: int32 - type: integer - successThreshold: - description: Minimum consecutive successes for the probe - to be considered successful after having failed. Defaults - to 1. Must be 1 for liveness and startup. Minimum - value is 1. - format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies an action involving - a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, - defaults to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: Number or name of the port to access - on the container. Number must be in the range - 1 to 65535. Name must be an IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: Optional duration in seconds the pod needs - to terminate gracefully upon probe failure. The grace - period is the duration in seconds after the processes - running in the pod are sent a termination signal and - the time when the processes are forcibly halted with - a kill signal. Set this value longer than the expected - cleanup time for your process. If this value is nil, - the pod's terminationGracePeriodSeconds will be used. - Otherwise, this value overrides the value provided - by the pod spec. Value must be non-negative integer. - The value zero indicates stop immediately via the - kill signal (no opportunity to shut down). This is - a beta field and requires enabling ProbeTerminationGracePeriod - feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds - is used if unset. - format: int64 - type: integer - timeoutSeconds: - description: 'Number of seconds after which the probe - times out. Defaults to 1 second. Minimum value is - 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' - format: int32 - type: integer - type: object storage: properties: size: diff --git a/config/crd/bases/database.marklogic.com_marklogicgroups.yaml b/config/crd/bases/database.marklogic.com_marklogicgroups.yaml index 6e192bc..f675cf6 100644 --- a/config/crd/bases/database.marklogic.com_marklogicgroups.yaml +++ b/config/crd/bases/database.marklogic.com_marklogicgroups.yaml @@ -1073,147 +1073,35 @@ spec: type: string type: object livenessProbe: - description: Probe describes a health check to be performed against - a container to determine whether it is alive or ready to receive - traffic. + default: + enabled: true + failureThreshold: 3 + initialDelaySeconds: 30 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 properties: - exec: - description: Exec specifies the action to take. - properties: - command: - description: Command is the command line to execute inside - the container, the working directory for the command is - root ('/') in the container's filesystem. The command is - simply exec'd, it is not run inside a shell, so traditional - shell instructions ('|', etc) won't work. To use a shell, - you need to explicitly call out to that shell. Exit status - of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - type: object + enabled: + type: boolean failureThreshold: - description: Minimum consecutive failures for the probe to be - considered failed after having succeeded. Defaults to 3. Minimum - value is 1. format: int32 + minimum: 0 type: integer - grpc: - description: GRPC specifies an action involving a GRPC port. - properties: - port: - description: Port number of the gRPC service. Number must - be in the range 1 to 65535. - format: int32 - type: integer - service: - description: "Service is the name of the service to place - in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - \n If this is not specified, the default behavior is defined - by gRPC." - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies the http request to perform. - properties: - host: - description: Host name to connect to, defaults to the pod - IP. You probably want to set "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP allows - repeated headers. - items: - description: HTTPHeader describes a custom header to be - used in HTTP probes - properties: - name: - description: The header field name. This will be canonicalized - upon output, so case-variant names will be understood - as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: Name or number of the port to access on the container. - Number must be in the range 1 to 65535. Name must be an - IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: Scheme to use for connecting to the host. Defaults - to HTTP. - type: string - required: - - port - type: object initialDelaySeconds: - description: 'Number of seconds after the container has started - before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' format: int32 + minimum: 0 type: integer periodSeconds: - description: How often (in seconds) to perform the probe. Default - to 10 seconds. Minimum value is 1. format: int32 + minimum: 0 type: integer successThreshold: - description: Minimum consecutive successes for the probe to be - considered successful after having failed. Defaults to 1. Must - be 1 for liveness and startup. Minimum value is 1. format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies an action involving a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: Number or name of the port to access on the container. - Number must be in the range 1 to 65535. Name must be an - IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: Optional duration in seconds the pod needs to terminate - gracefully upon probe failure. The grace period is the duration - in seconds after the processes running in the pod are sent a - termination signal and the time when the processes are forcibly - halted with a kill signal. Set this value longer than the expected - cleanup time for your process. If this value is nil, the pod's - terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. Value must - be non-negative integer. The value zero indicates stop immediately - via the kill signal (no opportunity to shut down). This is a - beta field and requires enabling ProbeTerminationGracePeriod - feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds - is used if unset. - format: int64 + minimum: 0 type: integer timeoutSeconds: - description: 'Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' format: int32 + minimum: 0 type: integer type: object name: @@ -1756,147 +1644,35 @@ spec: priorityClassName: type: string readinessProbe: - description: Probe describes a health check to be performed against - a container to determine whether it is alive or ready to receive - traffic. + default: + enabled: false + failureThreshold: 3 + initialDelaySeconds: 10 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 properties: - exec: - description: Exec specifies the action to take. - properties: - command: - description: Command is the command line to execute inside - the container, the working directory for the command is - root ('/') in the container's filesystem. The command is - simply exec'd, it is not run inside a shell, so traditional - shell instructions ('|', etc) won't work. To use a shell, - you need to explicitly call out to that shell. Exit status - of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - type: object + enabled: + type: boolean failureThreshold: - description: Minimum consecutive failures for the probe to be - considered failed after having succeeded. Defaults to 3. Minimum - value is 1. format: int32 + minimum: 0 type: integer - grpc: - description: GRPC specifies an action involving a GRPC port. - properties: - port: - description: Port number of the gRPC service. Number must - be in the range 1 to 65535. - format: int32 - type: integer - service: - description: "Service is the name of the service to place - in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - \n If this is not specified, the default behavior is defined - by gRPC." - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies the http request to perform. - properties: - host: - description: Host name to connect to, defaults to the pod - IP. You probably want to set "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP allows - repeated headers. - items: - description: HTTPHeader describes a custom header to be - used in HTTP probes - properties: - name: - description: The header field name. This will be canonicalized - upon output, so case-variant names will be understood - as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: Name or number of the port to access on the container. - Number must be in the range 1 to 65535. Name must be an - IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: Scheme to use for connecting to the host. Defaults - to HTTP. - type: string - required: - - port - type: object initialDelaySeconds: - description: 'Number of seconds after the container has started - before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' format: int32 + minimum: 0 type: integer periodSeconds: - description: How often (in seconds) to perform the probe. Default - to 10 seconds. Minimum value is 1. format: int32 + minimum: 0 type: integer successThreshold: - description: Minimum consecutive successes for the probe to be - considered successful after having failed. Defaults to 1. Must - be 1 for liveness and startup. Minimum value is 1. format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies an action involving a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: Number or name of the port to access on the container. - Number must be in the range 1 to 65535. Name must be an - IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: Optional duration in seconds the pod needs to terminate - gracefully upon probe failure. The grace period is the duration - in seconds after the processes running in the pod are sent a - termination signal and the time when the processes are forcibly - halted with a kill signal. Set this value longer than the expected - cleanup time for your process. If this value is nil, the pod's - terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. Value must - be non-negative integer. The value zero indicates stop immediately - via the kill signal (no opportunity to shut down). This is a - beta field and requires enabling ProbeTerminationGracePeriod - feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds - is used if unset. - format: int64 + minimum: 0 type: integer timeoutSeconds: - description: 'Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' format: int32 + minimum: 0 type: integer type: object replicas: @@ -2120,150 +1896,6 @@ spec: type: string type: object type: object - startupProbe: - description: Probe describes a health check to be performed against - a container to determine whether it is alive or ready to receive - traffic. - properties: - exec: - description: Exec specifies the action to take. - properties: - command: - description: Command is the command line to execute inside - the container, the working directory for the command is - root ('/') in the container's filesystem. The command is - simply exec'd, it is not run inside a shell, so traditional - shell instructions ('|', etc) won't work. To use a shell, - you need to explicitly call out to that shell. Exit status - of 0 is treated as live/healthy and non-zero is unhealthy. - items: - type: string - type: array - type: object - failureThreshold: - description: Minimum consecutive failures for the probe to be - considered failed after having succeeded. Defaults to 3. Minimum - value is 1. - format: int32 - type: integer - grpc: - description: GRPC specifies an action involving a GRPC port. - properties: - port: - description: Port number of the gRPC service. Number must - be in the range 1 to 65535. - format: int32 - type: integer - service: - description: "Service is the name of the service to place - in the gRPC HealthCheckRequest (see https://github.com/grpc/grpc/blob/master/doc/health-checking.md). - \n If this is not specified, the default behavior is defined - by gRPC." - type: string - required: - - port - type: object - httpGet: - description: HTTPGet specifies the http request to perform. - properties: - host: - description: Host name to connect to, defaults to the pod - IP. You probably want to set "Host" in httpHeaders instead. - type: string - httpHeaders: - description: Custom headers to set in the request. HTTP allows - repeated headers. - items: - description: HTTPHeader describes a custom header to be - used in HTTP probes - properties: - name: - description: The header field name. This will be canonicalized - upon output, so case-variant names will be understood - as the same header. - type: string - value: - description: The header field value - type: string - required: - - name - - value - type: object - type: array - path: - description: Path to access on the HTTP server. - type: string - port: - anyOf: - - type: integer - - type: string - description: Name or number of the port to access on the container. - Number must be in the range 1 to 65535. Name must be an - IANA_SVC_NAME. - x-kubernetes-int-or-string: true - scheme: - description: Scheme to use for connecting to the host. Defaults - to HTTP. - type: string - required: - - port - type: object - initialDelaySeconds: - description: 'Number of seconds after the container has started - before liveness probes are initiated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' - format: int32 - type: integer - periodSeconds: - description: How often (in seconds) to perform the probe. Default - to 10 seconds. Minimum value is 1. - format: int32 - type: integer - successThreshold: - description: Minimum consecutive successes for the probe to be - considered successful after having failed. Defaults to 1. Must - be 1 for liveness and startup. Minimum value is 1. - format: int32 - type: integer - tcpSocket: - description: TCPSocket specifies an action involving a TCP port. - properties: - host: - description: 'Optional: Host name to connect to, defaults - to the pod IP.' - type: string - port: - anyOf: - - type: integer - - type: string - description: Number or name of the port to access on the container. - Number must be in the range 1 to 65535. Name must be an - IANA_SVC_NAME. - x-kubernetes-int-or-string: true - required: - - port - type: object - terminationGracePeriodSeconds: - description: Optional duration in seconds the pod needs to terminate - gracefully upon probe failure. The grace period is the duration - in seconds after the processes running in the pod are sent a - termination signal and the time when the processes are forcibly - halted with a kill signal. Set this value longer than the expected - cleanup time for your process. If this value is nil, the pod's - terminationGracePeriodSeconds will be used. Otherwise, this - value overrides the value provided by the pod spec. Value must - be non-negative integer. The value zero indicates stop immediately - via the kill signal (no opportunity to shut down). This is a - beta field and requires enabling ProbeTerminationGracePeriod - feature gate. Minimum value is 1. spec.terminationGracePeriodSeconds - is used if unset. - format: int64 - type: integer - timeoutSeconds: - description: 'Number of seconds after which the probe times out. - Defaults to 1 second. Minimum value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' - format: int32 - type: integer - type: object storage: properties: size: diff --git a/config/samples/marklogicgroup.yaml b/config/samples/marklogicgroup.yaml index 8403012..800a45e 100644 --- a/config/samples/marklogicgroup.yaml +++ b/config/samples/marklogicgroup.yaml @@ -28,4 +28,18 @@ spec: # cpu: "2" # limits: # memory: "6Gi" - # cpu: "2" \ No newline at end of file + # cpu: "2" + # livenessProbe: + # enabled: true + # initialDelaySeconds: 31 + # periodSeconds: 11 + # timeoutSeconds: 6 + # successThreshold: 1 + # failureThreshold: 4 + # readinessProbe: + # enabled: true + # initialDelaySeconds: 29 + # periodSeconds: 9 + # timeoutSeconds: 4 + # successThreshold: 1 + # failureThreshold: 2 \ No newline at end of file diff --git a/pkg/k8sutil/configmap.go b/pkg/k8sutil/configmap.go index 4893033..a5f5772 100644 --- a/pkg/k8sutil/configmap.go +++ b/pkg/k8sutil/configmap.go @@ -86,9 +86,7 @@ func (oc *OperatorContext) getScriptsForConfigMap() map[string]string { if err != nil { logger.Error(err, "Error reading file") } - logger.Info(string(fileData)) configMapData[fileName] = string(fileData) } return configMapData } - diff --git a/pkg/k8sutil/statefulset.go b/pkg/k8sutil/statefulset.go index d8c840d..792595e 100644 --- a/pkg/k8sutil/statefulset.go +++ b/pkg/k8sutil/statefulset.go @@ -37,6 +37,8 @@ type containerParameters struct { LicenseKey string Licensee string BootstrapHost string + LivenessProbe databasev1alpha1.ContainerProbe + ReadinessProbe databasev1alpha1.ContainerProbe } func (oc *OperatorContext) ReconcileStatefulset() (reconcile.Result, error) { @@ -46,10 +48,11 @@ func (oc *OperatorContext) ReconcileStatefulset() (reconcile.Result, error) { annotations := map[string]string{} objectMeta := generateObjectMeta(cr.Spec.Name, cr.Namespace, labels, annotations) sts, err := oc.GetStatefulSet(cr.Namespace, objectMeta.Name) - + containerParams := generateContainerParams(cr) + statefulSetParams := generateStatefulSetsParams(cr) if err != nil { if apierrors.IsNotFound(err) { - statefulSetDef := generateStatefulSetsDef(objectMeta, generateStatefulSetsParams(cr), marklogicServerAsOwner(cr), generateContainerParams(cr)) + statefulSetDef := generateStatefulSetsDef(objectMeta, statefulSetParams, marklogicServerAsOwner(cr), containerParams) oc.createStatefulSet(cr.Namespace, statefulSetDef, cr) oc.Recorder.Event(oc.MarklogicGroup, "Normal", "StatefulSetCreated", "MarkLogic statefulSet created successfully") return result.RequeueSoon(10).Output() @@ -198,15 +201,19 @@ func generateContainerDef(name string, containerParams containerParameters) []co ImagePullPolicy: containerParams.ImagePullPolicy, Env: getEnvironmentVariables(containerParams), Lifecycle: getLifeCycle(), - ReadinessProbe: getReadinessProbe(), - LivenessProbe: getLivenessProbe(), - StartupProbe: getStartupProbe(), VolumeMounts: getVolumeMount(), }, } if containerParams.Resources != nil { containerDef[0].Resources = *containerParams.Resources } + if containerParams.LivenessProbe.Enabled == true { + containerDef[0].LivenessProbe = getLivenessProbe(containerParams.LivenessProbe) + } + + if containerParams.ReadinessProbe.Enabled == true { + containerDef[0].ReadinessProbe = getReadinessProbe(containerParams.ReadinessProbe) + } return containerDef } @@ -226,12 +233,14 @@ func generateStatefulSetsParams(cr *databasev1alpha1.MarklogicGroup) statefulSet func generateContainerParams(cr *databasev1alpha1.MarklogicGroup) containerParameters { trueProperty := true containerParams := containerParameters{ - Image: cr.Spec.Image, - Resources: cr.Spec.Resources, - Name: cr.Spec.Name, - Namespace: cr.Namespace, - ClusterDomain: cr.Spec.ClusterDomain, - BootstrapHost: cr.Spec.BootstrapHost, + Image: cr.Spec.Image, + Resources: cr.Spec.Resources, + Name: cr.Spec.Name, + Namespace: cr.Namespace, + ClusterDomain: cr.Spec.ClusterDomain, + BootstrapHost: cr.Spec.BootstrapHost, + LivenessProbe: cr.Spec.LivenessProbe, + ReadinessProbe: cr.Spec.ReadinessProbe, } if cr.Spec.Storage != nil { @@ -246,6 +255,7 @@ func generateContainerParams(cr *databasev1alpha1.MarklogicGroup) containerParam containerParams.LicenseKey = cr.Spec.License.Key containerParams.Licensee = cr.Spec.License.Licensee } + return containerParams } @@ -292,7 +302,7 @@ func generatePVCTemplate(storageSize string) corev1.PersistentVolumeClaim { pvcTemplate.CreationTimestamp = metav1.Time{} pvcTemplate.Name = "data" pvcTemplate.Spec.AccessModes = []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce} - // pvcTemplate.Spec.Resources.Requests.Storage().Add(resource.MustParse(storageSize)) + pvcTemplate.Spec.Resources.Requests.Storage().Add(resource.MustParse(storageSize)) pvcTemplate.Spec.Resources.Requests = corev1.ResourceList{ corev1.ResourceStorage: resource.MustParse(storageSize), } @@ -371,32 +381,28 @@ func getVolumeMount() []corev1.VolumeMount { return VolumeMounts } -func getLivenessProbe() *corev1.Probe { +func getLivenessProbe(probe databasev1alpha1.ContainerProbe) *corev1.Probe { return &corev1.Probe{ - InitialDelaySeconds: 30, - PeriodSeconds: 60, - FailureThreshold: 3, - TimeoutSeconds: 5, - SuccessThreshold: 1, + InitialDelaySeconds: probe.InitialDelaySeconds, + PeriodSeconds: probe.PeriodSeconds, + FailureThreshold: probe.FailureThreshold, + TimeoutSeconds: probe.TimeoutSeconds, + SuccessThreshold: probe.SuccessThreshold, ProbeHandler: corev1.ProbeHandler{ - HTTPGet: &corev1.HTTPGetAction{ - Path: "/", - Port: intstr.IntOrString{ - Type: intstr.Int, - IntVal: 7997, - }, + Exec: &corev1.ExecAction{ + Command: []string{"/bin/bash", "/tmp/helm-scripts/liveness-probe.sh"}, }, }, } } -func getReadinessProbe() *corev1.Probe { +func getReadinessProbe(probe databasev1alpha1.ContainerProbe) *corev1.Probe { return &corev1.Probe{ - InitialDelaySeconds: 10, - PeriodSeconds: 60, - FailureThreshold: 3, - TimeoutSeconds: 5, - SuccessThreshold: 1, + InitialDelaySeconds: probe.InitialDelaySeconds, + PeriodSeconds: probe.PeriodSeconds, + FailureThreshold: probe.FailureThreshold, + TimeoutSeconds: probe.TimeoutSeconds, + SuccessThreshold: probe.SuccessThreshold, ProbeHandler: corev1.ProbeHandler{ HTTPGet: &corev1.HTTPGetAction{ Path: "/", @@ -408,18 +414,3 @@ func getReadinessProbe() *corev1.Probe { }, } } - -func getStartupProbe() *corev1.Probe { - return &corev1.Probe{ - InitialDelaySeconds: 10, - PeriodSeconds: 20, - TimeoutSeconds: 3, - SuccessThreshold: 1, - FailureThreshold: 30, - ProbeHandler: corev1.ProbeHandler{ - Exec: &corev1.ExecAction{ - Command: []string{"ls", "/var/opt/MarkLogic/ready"}, - }, - }, - } -} From 043ab7e12f946f3b44ad4ecca2efa897db474a55 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Thu, 6 Jun 2024 00:32:34 -0700 Subject: [PATCH 09/12] reconcile groups --- api/v1alpha1/marklogicgroup_types.go | 3 ++- .../bases/database.marklogic.com_marklogicclusters.yaml | 5 ++++- .../crd/bases/database.marklogic.com_marklogicgroups.yaml | 5 ++++- config/samples/marklogicgroup.yaml | 3 +++ pkg/k8sutil/statefulset.go | 8 +++++++- 5 files changed, 20 insertions(+), 4 deletions(-) diff --git a/api/v1alpha1/marklogicgroup_types.go b/api/v1alpha1/marklogicgroup_types.go index e2f46a5..70069bc 100644 --- a/api/v1alpha1/marklogicgroup_types.go +++ b/api/v1alpha1/marklogicgroup_types.go @@ -61,7 +61,8 @@ type MarklogicGroupSpec struct { // +kubebuilder:default:={enabled: false, initialDelaySeconds: 10, timeoutSeconds: 5, periodSeconds: 30, successThreshold: 1, failureThreshold: 3} ReadinessProbe ContainerProbe `json:"readinessProbe,omitempty"` - GroupConfig GroupConfig `json:"groupConfigs,omitempty"` + // +kubebuilder:default:={name: "Default", enableXdqpSsl: true} + GroupConfig GroupConfig `json:"groupConfig,omitempty"` License *License `json:"license,omitempty"` EnableConverters bool `json:"enableConverters,omitempty"` diff --git a/config/crd/bases/database.marklogic.com_marklogicclusters.yaml b/config/crd/bases/database.marklogic.com_marklogicclusters.yaml index 502f064..5f5bcac 100644 --- a/config/crd/bases/database.marklogic.com_marklogicclusters.yaml +++ b/config/crd/bases/database.marklogic.com_marklogicclusters.yaml @@ -1165,7 +1165,10 @@ spec: type: boolean enableConverters: type: boolean - groupConfigs: + groupConfig: + default: + enableXdqpSsl: true + name: Default properties: enableXdqpSsl: type: boolean diff --git a/config/crd/bases/database.marklogic.com_marklogicgroups.yaml b/config/crd/bases/database.marklogic.com_marklogicgroups.yaml index f675cf6..0db5862 100644 --- a/config/crd/bases/database.marklogic.com_marklogicgroups.yaml +++ b/config/crd/bases/database.marklogic.com_marklogicgroups.yaml @@ -1040,7 +1040,10 @@ spec: type: boolean enableConverters: type: boolean - groupConfigs: + groupConfig: + default: + enableXdqpSsl: true + name: Default properties: enableXdqpSsl: type: boolean diff --git a/config/samples/marklogicgroup.yaml b/config/samples/marklogicgroup.yaml index 800a45e..6b95447 100644 --- a/config/samples/marklogicgroup.yaml +++ b/config/samples/marklogicgroup.yaml @@ -22,6 +22,9 @@ spec: license: key: "3981-CE27-75BB-9D3C-B81C-E067-1B39-DDFE-0875-C37E-D3F0-A76C-34E5-2F86-76BB-ADDD-E677-CB3F-D5FE-4773-C3CD-5EE8-87BC-36E5-3F71-0C15" licensee: "MarkLogic - Version 9 QA Test License" + groupConfig: + name: "enode" + # resources: # requests: # memory: "6Gi" diff --git a/pkg/k8sutil/statefulset.go b/pkg/k8sutil/statefulset.go index 792595e..a1df154 100644 --- a/pkg/k8sutil/statefulset.go +++ b/pkg/k8sutil/statefulset.go @@ -14,6 +14,7 @@ import ( "k8s.io/apimachinery/pkg/util/intstr" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "strconv" ) type statefulSetParameters struct { @@ -39,6 +40,7 @@ type containerParameters struct { BootstrapHost string LivenessProbe databasev1alpha1.ContainerProbe ReadinessProbe databasev1alpha1.ContainerProbe + GroupConfig databasev1alpha1.GroupConfig } func (oc *OperatorContext) ReconcileStatefulset() (reconcile.Result, error) { @@ -241,6 +243,7 @@ func generateContainerParams(cr *databasev1alpha1.MarklogicGroup) containerParam BootstrapHost: cr.Spec.BootstrapHost, LivenessProbe: cr.Spec.LivenessProbe, ReadinessProbe: cr.Spec.ReadinessProbe, + GroupConfig: cr.Spec.GroupConfig, } if cr.Spec.Storage != nil { @@ -328,7 +331,10 @@ func getEnvironmentVariables(containerParams containerParameters) []corev1.EnvVa Value: "false", }, corev1.EnvVar{ Name: "MARKLOGIC_GROUP", - Value: "Default", + Value: containerParams.GroupConfig.Name, + }, corev1.EnvVar{ + Name: "XDQP_SSL_ENABLED", + Value: strconv.FormatBool(containerParams.GroupConfig.EnableXdqpSsl), }, corev1.EnvVar{ Name: "MARKLOGIC_CLUSTER_TYPE", Value: "bootstrap", From 81609f96360640646ac58084e220c7806df12193 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Thu, 6 Jun 2024 08:30:25 -0700 Subject: [PATCH 10/12] add Unit Tests --- .../marklogicgroup_controller_test.go | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/internal/controller/marklogicgroup_controller_test.go b/internal/controller/marklogicgroup_controller_test.go index e8bde20..a6f01e0 100644 --- a/internal/controller/marklogicgroup_controller_test.go +++ b/internal/controller/marklogicgroup_controller_test.go @@ -39,15 +39,20 @@ const ( interval = time.Millisecond * 250 ) -var rep = int32(1) +var replicas = int32(2) var typeNamespaceName = types.NamespacedName{Name: Name, Namespace: Namespace} -var imageName = "marklogicdb/marklogic-db:11.1.0-centos-1.1.1" + +const imageName = "marklogicdb/marklogic-db:11.1.0-centos-1.1.1" + +var groupConfig = databasev1alpha1.GroupConfig{ + Name: "dnode", + EnableXdqpSsl: true, +} var _ = Describe("MarkLogicGroup controller", func() { Context("When creating an MarklogicGroup", func() { ctx := context.Background() It("Should create a MarklogicGroup CR, StatefulSet and Service", func() { - // Create the namespace ns := corev1.Namespace{ ObjectMeta: v1.ObjectMeta{Name: Namespace}, @@ -65,9 +70,10 @@ var _ = Describe("MarkLogicGroup controller", func() { Namespace: Namespace, }, Spec: databasev1alpha1.MarklogicGroupSpec{ - Replicas: &rep, - Name: Name, - Image: "marklogicdb/marklogic-db:11.1.0-centos-1.1.1", + Replicas: &replicas, + Name: Name, + Image: imageName, + GroupConfig: groupConfig, }, } Expect(k8sClient.Create(ctx, mlGroup)).Should(Succeed()) @@ -79,17 +85,20 @@ var _ = Describe("MarkLogicGroup controller", func() { return err == nil }, timeout, interval).Should(BeTrue()) Expect(createdCR.Spec.Image).Should(Equal(imageName)) - Expect(createdCR.Spec.Replicas).Should(Equal(&rep)) + Expect(createdCR.Spec.Replicas).Should(Equal(&replicas)) Expect(createdCR.Name).Should(Equal(Name)) + Expect(createdCR.Spec.GroupConfig).Should(Equal(groupConfig)) // Validating if StatefulSet is created successfully - createdSts := &appsv1.StatefulSet{} + sts := &appsv1.StatefulSet{} Eventually(func() bool { - err := k8sClient.Get(ctx, typeNamespaceName, createdSts) + err := k8sClient.Get(ctx, typeNamespaceName, sts) return err == nil }, timeout, interval).Should(BeTrue()) - Expect(createdSts.Spec.Template.Spec.Containers[0].Image).Should(Equal(imageName)) - Expect(createdSts.Spec.Replicas).Should(Equal(&rep)) + Expect(sts.Spec.Template.Spec.Containers[0].Image).Should(Equal(imageName)) + Expect(sts.Spec.Replicas).Should(Equal(&replicas)) + Expect(sts.Name).Should(Equal(Name)) + Expect(sts.Spec.PodManagementPolicy).Should(Equal(appsv1.ParallelPodManagement)) // Validating if Service is created successfully createdSrv := &corev1.Service{} @@ -99,5 +108,25 @@ var _ = Describe("MarkLogicGroup controller", func() { }, timeout, interval).Should(BeTrue()) Expect(createdSrv.Spec.Ports[0].TargetPort).Should(Equal(intstr.FromInt(int(7997)))) }) + + It("Should create configmap for MarkLogic scripts", func() { + // Validating if ConfigMap is created successfully + configMap := &corev1.ConfigMap{} + configMapName := Name + "-scripts" + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{Name: configMapName, Namespace: Namespace}, configMap) + return err == nil + }, timeout, interval).Should(BeTrue()) + }) + + It("Should create a secret for MarkLogic Admin User", func() { + // Validating if Secret is created successfully + secret := &corev1.Secret{} + secretName := Name + "-admin" + Eventually(func() bool { + err := k8sClient.Get(ctx, types.NamespacedName{Name: secretName, Namespace: Namespace}, secret) + return err == nil + }, timeout, interval).Should(BeTrue()) + }) }) }) From 60ee3f9b1264fab31a3451fd8121599db8e23b05 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Thu, 6 Jun 2024 09:55:22 -0700 Subject: [PATCH 11/12] remove licences --- config/samples/marklogicgroup.yaml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/config/samples/marklogicgroup.yaml b/config/samples/marklogicgroup.yaml index 6b95447..3933abb 100644 --- a/config/samples/marklogicgroup.yaml +++ b/config/samples/marklogicgroup.yaml @@ -9,7 +9,7 @@ metadata: app.kubernetes.io/created-by: marklogic-kubernetes-operator name: marklogicgroup-sample spec: - replicas: 1 + replicas: 2 name: marklogic image: "marklogicdb/marklogic-db:11.1.0-centos-1.1.2" auth: @@ -19,11 +19,8 @@ spec: # size: 10Gi terminationGracePeriodSeconds: 9 updateStrategy: OnDelete - license: - key: "3981-CE27-75BB-9D3C-B81C-E067-1B39-DDFE-0875-C37E-D3F0-A76C-34E5-2F86-76BB-ADDD-E677-CB3F-D5FE-4773-C3CD-5EE8-87BC-36E5-3F71-0C15" - licensee: "MarkLogic - Version 9 QA Test License" groupConfig: - name: "enode" + name: "node" # resources: # requests: From c78d9ae73e0a96495b4f83c4177975e47cedd052 Mon Sep 17 00:00:00 2001 From: Peng Zhou Date: Thu, 6 Jun 2024 10:04:36 -0700 Subject: [PATCH 12/12] change repo name --- config/manager/kustomization.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/manager/kustomization.yaml b/config/manager/kustomization.yaml index f3b8ec2..f85d4b0 100644 --- a/config/manager/kustomization.yaml +++ b/config/manager/kustomization.yaml @@ -4,5 +4,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization images: - name: controller - newName: pengzhouml/marklogic-kubernetes-operator - newTag: 0.3.1 + newName: marklogic/marklogic-kubernetes-operator + newTag: 0.4.1