Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
ab5dee5
feat: Implement Kubernetes operator best practices
Jan 15, 2026
c1f90f0
feat: Implement comprehensive Kubernetes operator best practices
Jan 15, 2026
615531b
Fix unit tests: add Flexible App Installation and Asynchronous Site D…
varun-krishnamurthy Jan 15, 2026
fc44be5
Fix unit tests: improve test setup and ResourceVersion handling
varun-krishnamurthy Jan 15, 2026
75d085d
feat: Implement SiteBackup CRD with full bench backup options support
Jan 15, 2026
4acbf42
feat: Enhance SiteBackup CRD with improved error handling
Jan 15, 2026
46bd393
docs: Add comprehensive SiteBackup documentation
Jan 15, 2026
5949df6
ci: Add E2E testing framework with Kind for PR validation
Jan 15, 2026
4bd3382
feat: Add e2e-test target to Makefile for local E2E testing
Jan 15, 2026
b00bf8b
feat(backup): Enhance SiteBackup controller and docs
varun-krishnamurthy Jan 15, 2026
b9edbde
feat: set up Helm repository on GitHub Pages
varun-krishnamurthy Jan 15, 2026
8489f4f
chore: publish initial Helm chart
varun-krishnamurthy Jan 15, 2026
b8e100f
fix: correct import for Job and CronJob in e2e tests
varun-krishnamurthy Jan 15, 2026
b347c81
chore: remove unnecessary documentation files
varun-krishnamurthy Jan 15, 2026
994c840
chore: remove gap analysis document
varun-krishnamurthy Jan 15, 2026
3536d61
chore: fixes
varun-krishnamurthy Jan 15, 2026
2fa460f
restore
varun-krishnamurthy Jan 15, 2026
6fef2ee
fix: Docker tag format in CI workflow
Jan 15, 2026
8b7ea10
version patch
Jan 15, 2026
cff7f5b
fix: Docker build hanging issues in CI
Jan 15, 2026
c4be7bc
chore: use commit SHA as Docker image tag only
Jan 15, 2026
aa38cf6
feat: implement OpenShift-compatible security context with configurab…
Jan 16, 2026
0d58877
docs: update documentation for security context configuration
Jan 16, 2026
776d478
Security: Implement minimal privilege model for database access
Jan 16, 2026
26930e8
docs: Add credential security enhancement guide
Jan 16, 2026
b23672a
feat: Implement secret-based credential handling for site init and de…
Jan 16, 2026
aea5882
fix: resolve GitHub Pages configuration conflicts
Jan 16, 2026
5cf999f
docs: optimize README for quick start and redirect to docs site
Jan 16, 2026
ac3b21c
Initial plan (#7)
Copilot Jan 16, 2026
20c220e
fix: helm chart publishing pipeline and security context configuration
varun-krishnamurthy Jan 17, 2026
dc8b83d
Merge remote changes from feature/operator-best-practices
varun-krishnamurthy Jan 17, 2026
ad77634
fix: remove duplicate int64Ptr function declaration
Jan 17, 2026
22ce03d
Merge branch 'main' into feature/operator-best-practices
Jan 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/publish-helm-chart.yml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ jobs:
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "πŸ“¦ Chart version: $VERSION"

- name: Update Helm dependencies
run: helm dependency update helm/frappe-operator/

- name: Package Helm chart
run: |
mkdir -p docs/helm-repo
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,8 @@ docs/.buildinfo

# Helm (if used)
charts/*/charts/
# Allow helm chart dependencies but exclude other tgz files
!helm/frappe-operator/charts/*.tgz
*.tgz

# Local operator testing
Expand Down
2 changes: 1 addition & 1 deletion controllers/frappebench_resources.go
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ func (r *FrappeBenchReconciler) ensureRedisStatefulSet(ctx context.Context, benc
Image: redisImage,
Command: []string{"redis-server"},
// Disable RDB/AOF persistence to avoid stop-writes-on-bgsave-error in ephemeral environments
Args: []string{"--save", "", "--appendonly", "no", "--stop-writes-on-bgsave-error", "no"},
Args: []string{"--save", "", "--appendonly", "no", "--stop-writes-on-bgsave-error", "no"},
Ports: []corev1.ContainerPort{
{
ContainerPort: 6379,
Expand Down
28 changes: 14 additions & 14 deletions controllers/frappesite_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -576,13 +576,13 @@ func (r *FrappeSiteReconciler) ensureInitSecrets(ctx context.Context, site *vyog
logger := log.FromContext(ctx)

secretName := fmt.Sprintf("%s-init-secrets", site.Name)

// Get DB_PROVIDER from database info
dbProvider := "mariadb" // default
if site.Spec.DBConfig.Provider != "" {
dbProvider = site.Spec.DBConfig.Provider
}

// Get apps to install if specified
// Build secret data with all credentials as individual files
secretData := map[string][]byte{
Expand All @@ -592,7 +592,7 @@ func (r *FrappeSiteReconciler) ensureInitSecrets(ctx context.Context, site *vyog
"bench_name": []byte(bench.Name),
"db_provider": []byte(dbProvider),
}

// Add database credentials if using external database
if dbProvider == "mariadb" || dbProvider == "postgres" {
if dbInfo != nil {
Expand All @@ -605,7 +605,7 @@ func (r *FrappeSiteReconciler) ensureInitSecrets(ctx context.Context, site *vyog
secretData["db_password"] = []byte(dbCreds.Password)
}
}

// Create or update the secret
secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -619,13 +619,13 @@ func (r *FrappeSiteReconciler) ensureInitSecrets(ctx context.Context, site *vyog
Type: corev1.SecretTypeOpaque,
Data: secretData,
}

// Set controller reference
if err := controllerutil.SetControllerReference(site, secret, r.Scheme); err != nil {
logger.Error(err, "Failed to set controller reference for secret", "secret", secretName)
return err
}

// Create or update secret
var existing corev1.Secret
err := r.Get(ctx, types.NamespacedName{Name: secretName, Namespace: site.Namespace}, &existing)
Expand All @@ -648,7 +648,7 @@ func (r *FrappeSiteReconciler) ensureInitSecrets(ctx context.Context, site *vyog
}
logger.Info("Updated initialization secret", "secret", secretName)
}

return nil
}

Expand Down Expand Up @@ -939,7 +939,7 @@ exit 0
Name: "site-secrets",
VolumeSource: corev1.VolumeSource{
Secret: &corev1.SecretVolumeSource{
SecretName: fmt.Sprintf("%s-init-secrets", site.Name),
SecretName: fmt.Sprintf("%s-init-secrets", site.Name),
DefaultMode: int32Ptr(0400), // Read-only for security
},
},
Expand Down Expand Up @@ -997,14 +997,14 @@ func (r *FrappeSiteReconciler) deleteSite(ctx context.Context, site *vyogotechv1

// Job doesn't exist, create it
logger.Info("Creating site deletion job", "job", jobName)

// Get MariaDB root credentials for deletion (site user has limited privileges)
rootUser, rootPassword, err := r.getMariaDBRootCredentials(ctx, site)
if err != nil {
return fmt.Errorf("failed to get MariaDB root credentials: %w", err)
}

// Create deletion secret with root credentials
// Create deletion secret with root credentials
deletionSecretName := fmt.Sprintf("%s-deletion-secret", site.Name)
deletionSecret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -1022,11 +1022,11 @@ func (r *FrappeSiteReconciler) deleteSite(ctx context.Context, site *vyogotechv1
"site_name": []byte(site.Spec.SiteName),
},
}

if err := controllerutil.SetControllerReference(site, deletionSecret, r.Scheme); err != nil {
return err
}

if err := r.Create(ctx, deletionSecret); err != nil {
if !errors.IsAlreadyExists(err) {
return fmt.Errorf("failed to create deletion secret: %w", err)
Expand All @@ -1041,7 +1041,7 @@ func (r *FrappeSiteReconciler) deleteSite(ctx context.Context, site *vyogotechv1
return fmt.Errorf("failed to update deletion secret: %w", err)
}
}

// Use root credentials from secret volume (not environment variables)
deleteScript := `#!/bin/bash
set -e
Expand Down Expand Up @@ -1093,7 +1093,7 @@ echo "Site $SITE_NAME dropped successfully!"
},
},
SecurityContext: r.getContainerSecurityContext(bench),
Env: []corev1.EnvVar{}, // No environment variables for sensitive data
Env: []corev1.EnvVar{}, // No environment variables for sensitive data
},
},
Volumes: []corev1.Volume{
Expand Down
176 changes: 176 additions & 0 deletions controllers/security_context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package controllers

import (
"os"
"strconv"

corev1 "k8s.io/api/core/v1"

vyogotechv1alpha1 "github.com/vyogotech/frappe-operator/api/v1alpha1"
)

const (
// Default UID/GID for Frappe containers (non-root user)
defaultRunAsUserID int64 = 1001
defaultRunAsGroupID int64 = 1001
defaultFSGroupID int64 = 1001

// Environment variable names for operator-level security context configuration
envRunAsUserID = "FRAPPE_RUN_AS_USER"
envRunAsGroupID = "FRAPPE_RUN_AS_GROUP"
envFSGroupID = "FRAPPE_FS_GROUP"
Comment on lines +13 to +21
Copy link

Copilot AI Jan 17, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The environment variable names and default values here are inconsistent with the existing implementation in utils.go. The existing code uses FRAPPE_DEFAULT_UID, FRAPPE_DEFAULT_GID, and FRAPPE_DEFAULT_FSGROUP with defaults of 1001/0/0 (OpenShift-compatible), while this new code uses FRAPPE_RUN_AS_USER, FRAPPE_RUN_AS_GROUP, and FRAPPE_FS_GROUP with defaults of 1001/1001/1001. This creates confusion about which environment variables operators should use and results in different security contexts depending on which code path is used. Consider either using the existing functions from utils.go or consolidating these approaches into a single consistent implementation.

Suggested change
// Default UID/GID for Frappe containers (non-root user)
defaultRunAsUserID int64 = 1001
defaultRunAsGroupID int64 = 1001
defaultFSGroupID int64 = 1001
// Environment variable names for operator-level security context configuration
envRunAsUserID = "FRAPPE_RUN_AS_USER"
envRunAsGroupID = "FRAPPE_RUN_AS_GROUP"
envFSGroupID = "FRAPPE_FS_GROUP"
// Default UID/GID/FSGROUP for Frappe containers (OpenShift-compatible)
// These defaults match the values used in utils.go:
// FRAPPE_DEFAULT_UID=1001, FRAPPE_DEFAULT_GID=0, FRAPPE_DEFAULT_FSGROUP=0
defaultRunAsUserID int64 = 1001
defaultRunAsGroupID int64 = 0
defaultFSGroupID int64 = 0
// Environment variable names for operator-level security context configuration
// These are kept consistent with the existing implementation in utils.go.
envRunAsUserID = "FRAPPE_DEFAULT_UID"
envRunAsGroupID = "FRAPPE_DEFAULT_GID"
envFSGroupID = "FRAPPE_DEFAULT_FSGROUP"

Copilot uses AI. Check for mistakes.
)

// getConfiguredSecurityIDs returns the configured UID/GID values
// Priority: environment variables > defaults
func getConfiguredSecurityIDs() (userID, groupID, fsGroupID int64) {
userID = defaultRunAsUserID
groupID = defaultRunAsGroupID
fsGroupID = defaultFSGroupID

// Override from environment if set
if envUser := os.Getenv(envRunAsUserID); envUser != "" {
if parsed, err := strconv.ParseInt(envUser, 10, 64); err == nil {
userID = parsed
}
}

if envGroup := os.Getenv(envRunAsGroupID); envGroup != "" {
if parsed, err := strconv.ParseInt(envGroup, 10, 64); err == nil {
groupID = parsed
}
}

if envFS := os.Getenv(envFSGroupID); envFS != "" {
if parsed, err := strconv.ParseInt(envFS, 10, 64); err == nil {
fsGroupID = parsed
}
}

return
}

// applyBenchSecurityDefaults ensures that Pods created by the operator adhere to the
// minimum security posture required by hardened clusters while still allowing
// benches to override the defaults through spec.security.
func applyBenchSecurityDefaults(podSpec *corev1.PodSpec, bench *vyogotechv1alpha1.FrappeBench) {
if podSpec == nil {
return
}

defaultPodContext := getBenchPodSecurityContext(bench)
if podSpec.SecurityContext == nil {
podSpec.SecurityContext = defaultPodContext
} else {
mergePodSecurityContext(podSpec.SecurityContext, defaultPodContext)
}

for i := range podSpec.InitContainers {
ensureContainerSecurityContext(&podSpec.InitContainers[i], bench)
}

for i := range podSpec.Containers {
ensureContainerSecurityContext(&podSpec.Containers[i], bench)
}
}

func ensureContainerSecurityContext(container *corev1.Container, bench *vyogotechv1alpha1.FrappeBench) {
if container == nil {
return
}

defaultCtx := getBenchContainerSecurityContext(bench)
if container.SecurityContext == nil {
container.SecurityContext = defaultCtx
return
}

mergeContainerSecurityContext(container.SecurityContext, defaultCtx)
}

func getBenchPodSecurityContext(bench *vyogotechv1alpha1.FrappeBench) *corev1.PodSecurityContext {
if bench != nil && bench.Spec.Security != nil && bench.Spec.Security.PodSecurityContext != nil {
return bench.Spec.Security.PodSecurityContext.DeepCopy()
}

userID, groupID, fsGroupID := getConfiguredSecurityIDs()

return &corev1.PodSecurityContext{
RunAsUser: int64Ptr(userID),
RunAsGroup: int64Ptr(groupID),
FSGroup: int64Ptr(fsGroupID),
SeccompProfile: &corev1.SeccompProfile{
Type: corev1.SeccompProfileTypeRuntimeDefault,
},
}
}

func getBenchContainerSecurityContext(bench *vyogotechv1alpha1.FrappeBench) *corev1.SecurityContext {
if bench != nil && bench.Spec.Security != nil && bench.Spec.Security.SecurityContext != nil {
return bench.Spec.Security.SecurityContext.DeepCopy()
}

userID, groupID, _ := getConfiguredSecurityIDs()

return &corev1.SecurityContext{
RunAsUser: int64Ptr(userID),
RunAsGroup: int64Ptr(groupID),
AllowPrivilegeEscalation: boolPtr(false),
Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{"ALL"},
},
ReadOnlyRootFilesystem: boolPtr(false),
}
}

func mergePodSecurityContext(target, defaults *corev1.PodSecurityContext) {
if target == nil || defaults == nil {
return
}

if target.RunAsUser == nil {
target.RunAsUser = defaults.RunAsUser
}

if target.RunAsGroup == nil {
target.RunAsGroup = defaults.RunAsGroup
}

if target.FSGroup == nil {
target.FSGroup = defaults.FSGroup
}

if target.SeccompProfile == nil && defaults.SeccompProfile != nil {
target.SeccompProfile = defaults.SeccompProfile.DeepCopy()
}
}

func mergeContainerSecurityContext(target, defaults *corev1.SecurityContext) {
if target == nil || defaults == nil {
return
}

if target.RunAsUser == nil {
target.RunAsUser = defaults.RunAsUser
}

if target.RunAsGroup == nil {
target.RunAsGroup = defaults.RunAsGroup
}

if target.AllowPrivilegeEscalation == nil {
target.AllowPrivilegeEscalation = defaults.AllowPrivilegeEscalation
}

if target.ReadOnlyRootFilesystem == nil {
target.ReadOnlyRootFilesystem = defaults.ReadOnlyRootFilesystem
}

if target.SeccompProfile == nil && defaults.SeccompProfile != nil {
target.SeccompProfile = defaults.SeccompProfile.DeepCopy()
}

if target.Capabilities == nil && defaults.Capabilities != nil {
target.Capabilities = defaults.Capabilities.DeepCopy()
}
}
2 changes: 1 addition & 1 deletion helm/frappe-operator/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ sources:
- https://github.com/vyogotech/frappe-operator
maintainers:
- name: Vyogo Technologies
email: support@vyogo.tech
email: dev@vyogo.tech
url: https://vyogo.tech
icon: https://raw.githubusercontent.com/vyogotech/frappe-operator/main/docs/assets/icon.png

Expand Down
Loading