feat: create SecurityContext for k3s scheduler from docker-options

Closes #7664
This commit is contained in:
Jose Diaz-Gonzalez
2025-09-12 18:09:33 -04:00
parent fa0c9663cf
commit f90a4061c0
7 changed files with 199 additions and 14 deletions

View File

@@ -568,6 +568,11 @@ This plugin implements various functionality through `plugn` triggers to integra
- `apps:clone`
- `apps:destroy`
- `apps:rename`
- `docker-options`:
- The following docker options are translated into their kubernetes equivalents:
- `--cap-add`
- `--cap-drop`
- `--privileged`
- `cron`
- `enter`
- `deploy`

View File

@@ -6,6 +6,8 @@ import (
"os"
"sort"
"strings"
"github.com/dokku/dokku/plugins/common"
)
// SetDockerOptionForPhases sets an option to specified phases
@@ -85,3 +87,51 @@ func GetDockerOptionsForPhase(appName string, phase string) ([]string, error) {
return options, nil
}
// GetSpecifiedDockerOptionsForPhase returns the docker options for the specified phase that are in the desiredOptions list
// It expects desiredOptions to be a list of docker options that are in the format "--option"
// And will retrieve any lines that start with the desired option
func GetSpecifiedDockerOptionsForPhase(appName string, phase string, desiredOptions []string) (map[string][]string, error) {
foundOptions := map[string][]string{}
options, err := GetDockerOptionsForPhase(appName, phase)
if err != nil {
return foundOptions, err
}
for _, option := range options {
for _, desiredOption := range desiredOptions {
if option == desiredOption {
foundOptions[desiredOption] = []string{}
break
}
parts := strings.SplitN(option, " ", 2)
if len(parts) != 2 {
common.LogWarn(fmt.Sprintf("Invalid docker option found for %s: %s", appName, option))
continue
}
// match options that are in the format "--option=value"
if strings.HasPrefix(option, fmt.Sprintf("%s=", desiredOption)) {
if _, ok := foundOptions[desiredOption]; !ok {
foundOptions[desiredOption] = []string{}
}
foundOptions[desiredOption] = append(foundOptions[desiredOption], parts[1])
break
}
// match options that are in the format "--option value"
if strings.HasPrefix(option, fmt.Sprintf("%s ", desiredOption)) {
if _, ok := foundOptions[desiredOption]; !ok {
foundOptions[desiredOption] = []string{}
}
foundOptions[desiredOption] = append(foundOptions[desiredOption], parts[1])
break
}
}
}
return foundOptions, nil
}

View File

@@ -18,6 +18,7 @@ import (
appjson "github.com/dokku/dokku/plugins/app-json"
"github.com/dokku/dokku/plugins/common"
dockeroptions "github.com/dokku/dokku/plugins/docker-options"
"github.com/dokku/dokku/plugins/logs"
nginxvhosts "github.com/dokku/dokku/plugins/nginx-vhosts"
resty "github.com/go-resty/resty/v2"
@@ -1489,6 +1490,37 @@ func getStartCommand(input StartCommandInput) (StartCommandOutput, error) {
}, nil
}
func getSecurityContext(appName string, phase string) (SecurityContext, error) {
securityContext := SecurityContext{}
deployOptions, err := dockeroptions.GetSpecifiedDockerOptionsForPhase(appName, phase, []string{
"--cap-add",
"--cap-drop",
"--privileged",
})
if err != nil {
return SecurityContext{}, fmt.Errorf("Error getting deploy options: %w", err)
}
if _, ok := deployOptions["--privileged"]; ok {
securityContext.Privileged = true
}
if capAdd, ok := deployOptions["--cap-add"]; ok {
capabilities := []string{}
for _, cap := range capAdd {
capabilities = append(capabilities, strings.ToUpper(cap))
}
securityContext.Capabilities.Add = capabilities
}
if capDrop, ok := deployOptions["--cap-drop"]; ok {
capabilities := []string{}
for _, cap := range capDrop {
capabilities = append(capabilities, strings.ToUpper(cap))
}
securityContext.Capabilities.Drop = capabilities
}
return securityContext, nil
}
func getProcessSpecificKustomizeRootPath(appName string) string {
if !hasKustomizeDirectory(appName) {
return ""

View File

@@ -3,6 +3,7 @@ package scheduler_k3s
import (
"crypto/rand"
"fmt"
"maps"
"os"
"strings"
@@ -41,15 +42,16 @@ type AppValues struct {
}
type GlobalValues struct {
Annotations ProcessAnnotations `yaml:"annotations,omitempty"`
AppName string `yaml:"app_name"`
DeploymentID string `yaml:"deployment_id"`
Image GlobalImage `yaml:"image"`
Labels ProcessLabels `yaml:"labels,omitempty"`
Keda GlobalKedaValues `yaml:"keda"`
Namespace string `yaml:"namespace"`
Network GlobalNetwork `yaml:"network"`
Secrets map[string]string `yaml:"secrets,omitempty"`
Annotations ProcessAnnotations `yaml:"annotations,omitempty"`
AppName string `yaml:"app_name"`
DeploymentID string `yaml:"deployment_id"`
Image GlobalImage `yaml:"image"`
Labels ProcessLabels `yaml:"labels,omitempty"`
Keda GlobalKedaValues `yaml:"keda"`
Namespace string `yaml:"namespace"`
Network GlobalNetwork `yaml:"network"`
Secrets map[string]string `yaml:"secrets,omitempty"`
SecurityContext SecurityContext `yaml:"security_context,omitempty"`
}
type GlobalImage struct {
@@ -351,11 +353,53 @@ type Job struct {
Namespace string
ProcessType string
Schedule string
SecurityContext SecurityContext
Suffix string
RemoveContainer bool
WorkingDir string
}
// SecurityContext contains the security context for a process
type SecurityContext struct {
// Capabilities contains the capabilities for a process
Capabilities SecurityContextCapabilities `yaml:"capabilities,omitempty"`
// Privileged contains the privileged flag for a process
Privileged bool `yaml:"privileged,omitempty"`
}
// ToCoreV1SecurityContext converts the security context to a corev1.SecurityContext
func (s SecurityContext) ToCoreV1SecurityContext() corev1.SecurityContext {
securityContext := corev1.SecurityContext{
Capabilities: &corev1.Capabilities{},
Privileged: ptr.To(s.Privileged),
}
if len(s.Capabilities.Add) > 0 {
capabilities := make([]corev1.Capability, len(s.Capabilities.Add))
for i, cap := range s.Capabilities.Add {
capabilities[i] = corev1.Capability(cap)
}
securityContext.Capabilities.Add = capabilities
}
if len(s.Capabilities.Drop) > 0 {
capabilities := make([]corev1.Capability, len(s.Capabilities.Drop))
for i, cap := range s.Capabilities.Drop {
capabilities[i] = corev1.Capability(cap)
}
securityContext.Capabilities.Drop = capabilities
}
return securityContext
}
// SecurityContextCapabilities contains the capabilities for a process
type SecurityContextCapabilities struct {
// Add contains the add capabilities for a process
Add []string `yaml:"add,omitempty"`
// Drop contains the drop capabilities for a process
Drop []string `yaml:"drop,omitempty"`
}
func templateKubernetesJob(input Job) (batchv1.Job, error) {
labels := map[string]string{
"app.kubernetes.io/instance": fmt.Sprintf("%s-%s", input.AppName, input.ProcessType),
@@ -368,9 +412,7 @@ func templateKubernetesJob(input Job) (batchv1.Job, error) {
"dokku.com/managed": "true",
}
for key, value := range input.Labels {
labels[key] = value
}
maps.Copy(labels, input.Labels)
secretName := fmt.Sprintf("env-%s.%d", input.AppName, input.DeploymentID)
env := []corev1.EnvVar{}
@@ -417,6 +459,8 @@ func templateKubernetesJob(input Job) (batchv1.Job, error) {
podAnnotations[key] = value
}
securityContext := input.SecurityContext.ToCoreV1SecurityContext()
job := batchv1.Job{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s-%s", input.AppName, input.ProcessType, suffix),
@@ -453,7 +497,8 @@ func templateKubernetesJob(input Job) (batchv1.Job, error) {
Limits: corev1.ResourceList{},
Requests: corev1.ResourceList{},
},
WorkingDir: input.WorkingDir,
SecurityContext: &securityContext,
WorkingDir: input.WorkingDir,
},
},
RestartPolicy: corev1.RestartPolicyNever,

View File

@@ -102,6 +102,26 @@ spec:
{{- end }}
{{- end }}
{{- end }}
{{- if hasKey $.Values.global "security_context" }}
securityContext:
{{- if $.Values.global.security_context.privileged }}
privileged: true
{{- end }}
{{- if hasKey $.Values.global.security_context "capabilities" }}
capabilities:
{{- if hasKey $.Values.global.security_context.capabilities "add" }}
add:
{{- range $.Values.global.security_context.capabilities.add }}
- {{ . }}
{{- end }}
{{- end }}
{{- if hasKey $.Values.global.security_context.capabilities "drop" }}
drop:
{{- range $.Values.global.security_context.capabilities.drop }}
- {{ . }}
{{- end }}
{{- end }}
{{- end }}
{{- if $.Values.global.image.working_dir }}
workingDir: {{ $.Values.global.image.working_dir }}
{{- end }}

View File

@@ -117,6 +117,27 @@ spec:
readinessProbe:
{{ $config.healthchecks.readiness | toJson | indent 10 }}
{{- end }}
{{- if hasKey $.Values.global "security_context" }}
securityContext:
{{- if $.Values.global.security_context.privileged }}
privileged: true
{{- end }}
{{- if hasKey $.Values.global.security_context "capabilities" }}
capabilities:
{{- if hasKey $.Values.global.security_context.capabilities "add" }}
add:
{{- range $.Values.global.security_context.capabilities.add }}
- {{ . }}
{{- end }}
{{- end }}
{{- if hasKey $.Values.global.security_context.capabilities "drop" }}
drop:
{{- range $.Values.global.security_context.capabilities.drop }}
- {{ . }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- if $.Values.global.image.working_dir }}
workingDir: {{ $.Values.global.image.working_dir }}
{{- end }}

View File

@@ -387,6 +387,11 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e
return fmt.Errorf("Error getting keda values: %w", err)
}
securityContext, err := getSecurityContext(appName, "deploy")
if err != nil {
return fmt.Errorf("Error getting security context: %w", err)
}
values := &AppValues{
Global: GlobalValues{
Annotations: globalAnnotations,
@@ -407,7 +412,8 @@ func TriggerSchedulerDeploy(scheduler string, appName string, imageTag string) e
PrimaryPort: primaryPort,
PrimaryServicePort: primaryServicePort,
},
Secrets: map[string]string{},
Secrets: map[string]string{},
SecurityContext: securityContext,
},
Processes: map[string]ProcessValues{},
}
@@ -1228,6 +1234,11 @@ func TriggerSchedulerRun(scheduler string, appName string, envCount int, args []
}
}
securityContext, err := getSecurityContext(appName, "run")
if err != nil {
return fmt.Errorf("Error getting security context: %w", err)
}
workingDir := common.GetWorkingDir(appName, image)
job, err := templateKubernetesJob(Job{
AppName: appName,
@@ -1243,6 +1254,7 @@ func TriggerSchedulerRun(scheduler string, appName string, envCount int, args []
Namespace: namespace,
ProcessType: processType,
RemoveContainer: rmContainer,
SecurityContext: securityContext,
WorkingDir: workingDir,
})
if err != nil {