diff --git a/Jenkinsfile b/Jenkinsfile index 855dd71..0bb7b64 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -157,12 +157,19 @@ pipeline { buildDiscarder logRotator(artifactDaysToKeepStr: '20', artifactNumToKeepStr: '', daysToKeepStr: '30', numToKeepStr: '') skipStagesAfterUnstable() } - // triggers { - // //TODO: add scheduled runs - // } - // environment { - // //TODO - // } + + triggers { + // Trigger nightly builds on the develop branch + parameterizedCron( env.BRANCH_NAME == 'develop' ? '''00 05 * * * % E2E_MARKLOGIC_IMAGE_VERSION=ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi-rootless:latest-12 + 00 05 * * * % E2E_MARKLOGIC_IMAGE_VERSION=ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi-rootless:latest-11; PUBLISH_IMAGE=false''' : '') + } + + environment { + PATH = "/space/go/bin:${env.PATH}" + MINIKUBE_HOME = "/space/minikube/" + KUBECONFIG = "/space/.kube-config" + GOPATH = "/space/go" + } parameters { string(name: 'E2E_MARKLOGIC_IMAGE_VERSION', defaultValue: 'ml-docker-db-dev-tierpoint.bed-artifactory.bedford.progress.com/marklogic/marklogic-server-ubi-rootless:latest-12', description: 'Docker image to use for tests.', trim: true) diff --git a/config/samples/complete.yaml b/config/samples/complete.yaml index 35015ee..fac1ed0 100644 --- a/config/samples/complete.yaml +++ b/config/samples/complete.yaml @@ -120,12 +120,16 @@ spec: terminationGracePeriodSeconds: 10 updateStrategy: OnDelete podSecurityContext: - fsGroup: 2 + fsGroup: 2 # MarkLogic runs as user 1000 and group 2 (mlusers) **!!Must not be changed!!** fsGroupChangePolicy: OnRootMismatch securityContext: - runAsUser: 1000 + runAsUser: 1000 # MarkLogic runs as user 1000 and group 2 (mlusers) **!!Must not be changed!!** runAsNonRoot: true allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - "ALL" ## Node Affinity for pod-node scheduling constraints ## ref: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/#affinity-and-anti-affinity affinity: {} diff --git a/pkg/k8sutil/statefulset.go b/pkg/k8sutil/statefulset.go index e62d192..856db24 100644 --- a/pkg/k8sutil/statefulset.go +++ b/pkg/k8sutil/statefulset.go @@ -18,6 +18,114 @@ import ( "sigs.k8s.io/controller-runtime/pkg/reconcile" ) +// getDefaultPodSecurityContext returns the default pod-level security context for MarkLogic StatefulSets +// MarkLogic runs as user 1000 and group 2 (mlusers) - Must not be changed +func getDefaultPodSecurityContext() *corev1.PodSecurityContext { + fsGroup := int64(2) + fsGroupChangePolicy := corev1.FSGroupChangeOnRootMismatch + return &corev1.PodSecurityContext{ + FSGroup: &fsGroup, + FSGroupChangePolicy: &fsGroupChangePolicy, + } +} + +// getDefaultContainerSecurityContext returns the default container-level security context for MarkLogic containers +// This enforces strict security requirements: +// - runAsUser: 1000 - MarkLogic runs as user 1000 and group 2 (mlusers) - Must not be changed +// - runAsNonRoot: true (prevents running as root) +// - allowPrivilegeEscalation: false (prevents privilege escalation) +// - readOnlyRootFilesystem: true (makes root filesystem read-only) +// - capabilities drop ALL (removes all Linux capabilities) +func getDefaultContainerSecurityContext() *corev1.SecurityContext { + runAsUser := int64(1000) + runAsNonRoot := true + allowPrivilegeEscalation := false + readOnlyRootFilesystem := true + return &corev1.SecurityContext{ + RunAsUser: &runAsUser, + RunAsNonRoot: &runAsNonRoot, + AllowPrivilegeEscalation: &allowPrivilegeEscalation, + ReadOnlyRootFilesystem: &readOnlyRootFilesystem, + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"ALL"}, + }, + } +} + +// mergeSecurityContext merges user-provided SecurityContext with defaults +// User-provided values take precedence over defaults for flexibility +func mergeSecurityContext(userContext, defaultContext *corev1.SecurityContext) *corev1.SecurityContext { + if userContext == nil { + return defaultContext + } + + merged := defaultContext.DeepCopy() + + if userContext.RunAsUser != nil { + merged.RunAsUser = userContext.RunAsUser + } + if userContext.RunAsNonRoot != nil { + merged.RunAsNonRoot = userContext.RunAsNonRoot + } + if userContext.AllowPrivilegeEscalation != nil { + merged.AllowPrivilegeEscalation = userContext.AllowPrivilegeEscalation + } + if userContext.ReadOnlyRootFilesystem != nil { + merged.ReadOnlyRootFilesystem = userContext.ReadOnlyRootFilesystem + } + if userContext.Capabilities != nil { + merged.Capabilities = userContext.Capabilities + } + if userContext.Privileged != nil { + merged.Privileged = userContext.Privileged + } + if userContext.SELinuxOptions != nil { + merged.SELinuxOptions = userContext.SELinuxOptions + } + if userContext.SeccompProfile != nil { + merged.SeccompProfile = userContext.SeccompProfile + } + + return merged +} + +// mergePodSecurityContext merges user-provided PodSecurityContext with defaults +// User-provided values take precedence over defaults for flexibility +func mergePodSecurityContext(userContext, defaultContext *corev1.PodSecurityContext) *corev1.PodSecurityContext { + if userContext == nil { + return defaultContext + } + + merged := defaultContext.DeepCopy() + + if userContext.FSGroup != nil { + merged.FSGroup = userContext.FSGroup + } + if userContext.FSGroupChangePolicy != nil { + merged.FSGroupChangePolicy = userContext.FSGroupChangePolicy + } + if userContext.RunAsUser != nil { + merged.RunAsUser = userContext.RunAsUser + } + if userContext.RunAsNonRoot != nil { + merged.RunAsNonRoot = userContext.RunAsNonRoot + } + if userContext.SELinuxOptions != nil { + merged.SELinuxOptions = userContext.SELinuxOptions + } + if userContext.SeccompProfile != nil { + merged.SeccompProfile = userContext.SeccompProfile + } + if userContext.SupplementalGroups != nil { + merged.SupplementalGroups = userContext.SupplementalGroups + } + if userContext.Sysctls != nil { + merged.Sysctls = userContext.Sysctls + } + + return merged +} + type statefulSetParameters struct { Replicas *int32 Name string @@ -88,15 +196,44 @@ func (oc *OperatorContext) ReconcileStatefulset() (reconcile.Result, error) { oc.Recorder.Event(oc.MarklogicGroup, "Normal", "StatefulSetCreated", "MarkLogic statefulSet created successfully") return result.Done().Output() } - _, outputErr := result.Error(err).Output() - if outputErr != nil { - logger.Error(outputErr, "Failed to process result error") - } + logger.Error(err, "Cannot get statefulSet for MarkLogic") + return result.Error(err).Output() } + + patchDiff, err := patch.DefaultPatchMaker.Calculate(currentSts, statefulSetDef, + patch.IgnoreStatusFields(), + patch.IgnoreVolumeClaimTemplateTypeMetaAndStatus(), + patch.IgnoreField("kind")) + logger.Info("Patch Diff:", "Diff", patchDiff.String()) + logger.Info("statefulSetDef Spec:", "Spec", statefulSetDef.Spec.Replicas) if err != nil { - logger.Error(err, "Cannot create standalone statefulSet for MarkLogic") + logger.Error(err, "Error calculating patch") return result.Error(err).Output() } + + if !patchDiff.IsEmpty() { + logger.Info("MarkLogic statefulSet spec is different from the MarkLogicGroup spec, updating the statefulSet") + currentSts.Spec = statefulSetDef.Spec + currentSts.ObjectMeta.Annotations = statefulSetDef.ObjectMeta.Annotations + currentSts.ObjectMeta.Labels = statefulSetDef.ObjectMeta.Labels + err := oc.Client.Update(oc.Ctx, currentSts) + if err != nil { + logger.Error(err, "Error updating statefulSet") + return result.Error(err).Output() + } + } else { + logger.Info("MarkLogic statefulSet spec is the same as the current spec, no update needed") + } + logger.Info("Operator Status:", "Stage", cr.Status.Stage) + if cr.Status.Stage == "STS_CREATED" { + logger.Info("MarkLogic statefulSet created successfully, waiting for pods to be ready") + pods, err := GetPodsForStatefulSet(oc.Ctx, cr.Namespace, cr.Spec.Name) + if err != nil { + logger.Error(err, "Error getting pods for statefulset") + } + logger.Info("Pods in statefulSet: ", "Pods", pods) + } + patchClient := client.MergeFrom(oc.MarklogicGroup.DeepCopy()) updated := false if currentSts.Status.ReadyReplicas == 0 || currentSts.Status.ReadyReplicas != currentSts.Status.Replicas { @@ -132,37 +269,6 @@ func (oc *OperatorContext) ReconcileStatefulset() (reconcile.Result, error) { } } - patchDiff, err := patch.DefaultPatchMaker.Calculate(currentSts, statefulSetDef, - patch.IgnoreStatusFields(), - patch.IgnoreVolumeClaimTemplateTypeMetaAndStatus(), - patch.IgnoreField("kind")) - if err != nil { - logger.Error(err, "Error calculating patch") - return result.Error(err).Output() - } - if !patchDiff.IsEmpty() { - logger.Info("MarkLogic statefulSet spec is different from the MarkLogicGroup spec, updating the statefulSet") - currentSts.Spec = statefulSetDef.Spec - currentSts.ObjectMeta.Annotations = statefulSetDef.ObjectMeta.Annotations - currentSts.ObjectMeta.Labels = statefulSetDef.ObjectMeta.Labels - err := oc.Client.Update(oc.Ctx, currentSts) - if err != nil { - logger.Error(err, "Error updating statefulSet") - return result.Error(err).Output() - } - } else { - logger.Info("MarkLogic statefulSet spec is the same as the current spec, no update needed") - } - logger.Info("Operator Status:", "Stage", cr.Status.Stage) - if cr.Status.Stage == "STS_CREATED" { - logger.Info("MarkLogic statefulSet created successfully, waiting for pods to be ready") - pods, err := GetPodsForStatefulSet(cr.Namespace, cr.Spec.Name) - if err != nil { - logger.Error(err, "Error getting pods for statefulset") - } - logger.Info("Pods in statefulSet: ", "Pods", pods) - } - return result.Done().Output() } @@ -180,7 +286,7 @@ func (oc *OperatorContext) setCondition(condition *metav1.Condition) bool { func (oc *OperatorContext) GetStatefulSet(namespace string, stateful string) (*appsv1.StatefulSet, error) { logger := oc.ReqLogger statefulInfo := &appsv1.StatefulSet{} - err := oc.Client.Get(context.TODO(), client.ObjectKey{Namespace: namespace, Name: stateful}, statefulInfo) + err := oc.Client.Get(oc.Ctx, client.ObjectKey{Namespace: namespace, Name: stateful}, statefulInfo) if err != nil { logger.Info("MarkLogic statefulSet get action failed") return nil, err @@ -191,7 +297,7 @@ func (oc *OperatorContext) GetStatefulSet(namespace string, stateful string) (*a func (oc *OperatorContext) createStatefulSet(statefulset *appsv1.StatefulSet, cr *marklogicv1.MarklogicGroup) error { logger := oc.ReqLogger - err := oc.Client.Create(context.TODO(), statefulset) + err := oc.Client.Create(oc.Ctx, statefulset) if err != nil { logger.Error(err, "MarkLogic stateful creation failed") return err @@ -202,6 +308,10 @@ func (oc *OperatorContext) createStatefulSet(statefulset *appsv1.StatefulSet, cr } func generateStatefulSetsDef(stsMeta metav1.ObjectMeta, params statefulSetParameters, ownerDef metav1.OwnerReference, containerParams containerParameters) *appsv1.StatefulSet { + // Enforce default pod security context, merging with user-provided values + // This ensures all MarkLogic pods run with secure defaults + podSecurityContext := mergePodSecurityContext(containerParams.PodSecurityContext, getDefaultPodSecurityContext()) + statefulSet := &appsv1.StatefulSet{ TypeMeta: generateTypeMeta("StatefulSet", "apps/v1"), ObjectMeta: stsMeta, @@ -219,7 +329,7 @@ func generateStatefulSetsDef(stsMeta metav1.ObjectMeta, params statefulSetParame Spec: corev1.PodSpec{ Containers: generateContainerDef("marklogic-server", containerParams), TerminationGracePeriodSeconds: params.TerminationGracePeriodSeconds, - SecurityContext: containerParams.PodSecurityContext, + SecurityContext: podSecurityContext, Volumes: generateVolumes(stsMeta.Name, containerParams), NodeSelector: params.NodeSelector, Affinity: params.Affinity, @@ -306,11 +416,11 @@ func generateStatefulSetsDef(stsMeta metav1.ObjectMeta, params statefulSetParame return statefulSet } -func GetPodsForStatefulSet(namespace, name string) ([]corev1.Pod, error) { +func GetPodsForStatefulSet(ctx context.Context, namespace, name string) ([]corev1.Pod, error) { selector := fmt.Sprintf("app.kubernetes.io/name=marklogic,app.kubernetes.io/instance=%s", name) // List Pods with the label selector listOptions := metav1.ListOptions{LabelSelector: selector} - pods, err := GenerateK8sClient().CoreV1().Pods(namespace).List(context.TODO(), listOptions) + pods, err := GenerateK8sClient().CoreV1().Pods(namespace).List(ctx, listOptions) if err != nil { return nil, err } @@ -319,6 +429,10 @@ func GetPodsForStatefulSet(namespace, name string) ([]corev1.Pod, error) { } func generateContainerDef(name string, containerParams containerParameters) []corev1.Container { + // Enforce default container security context, merging with user-provided values + // This ensures all MarkLogic containers run with strict security settings + securityContext := mergeSecurityContext(containerParams.SecurityContext, getDefaultContainerSecurityContext()) + containerDef := []corev1.Container{ { Name: name, @@ -326,7 +440,7 @@ func generateContainerDef(name string, containerParams containerParameters) []co ImagePullPolicy: containerParams.ImagePullPolicy, Env: getEnvironmentVariables(containerParams), Lifecycle: getLifeCycle(), - SecurityContext: containerParams.SecurityContext, + SecurityContext: securityContext, VolumeMounts: getVolumeMount(containerParams), }, }