Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
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
113 changes: 92 additions & 21 deletions src/pkg/k8s.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (

const (
ContainerNameHelper = "helper"
ContainerNameInit = "init"
ContainerNameJob = "job"
)

Expand Down Expand Up @@ -110,9 +111,15 @@ func NewJobRunner(runnerId string, path string) *JobRunner {
}
}

func (s *JobRunner) getPodEnv(configs []opslevel.RunnerJobVariable) []corev1.EnvVar {
// getPodEnv returns the env vars to inject into a container for the given
// scope. Variables with no Scope set are visible to every container; variables
// with a Scope are only visible to containers running in that scope.
func (s *JobRunner) getPodEnv(configs []opslevel.RunnerJobVariable, scope opslevel.RunnerJobVariableScope) []corev1.EnvVar {
output := make([]corev1.EnvVar, 0)
for _, config := range configs {
if config.Scope != "" && config.Scope != scope {
continue
}
output = append(output, corev1.EnvVar{
Name: config.Key,
Value: config.Value,
Expand Down Expand Up @@ -195,6 +202,30 @@ func (s *JobRunner) getPodObject(identifier string, labels map[string]string, jo
}
}

initContainers := []corev1.Container{
{
Name: ContainerNameHelper,
Image: s.podConfig.helperImage(),
ImagePullPolicy: s.podConfig.PullPolicy,
Command: []string{
"cp",
"/opslevel-runner",
"/mount",
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "shared",
ReadOnly: false,
MountPath: "/mount",
},
},
},
}

if len(job.InitCommands) > 0 {
initContainers = append(initContainers, s.getInitContainer(job, containerSecurityContext))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

in theory we could make the helper container conditional too, only SBOM jobs use it

}

return &corev1.Pod{
ObjectMeta: metav1.ObjectMeta{
Name: identifier,
Expand All @@ -208,25 +239,7 @@ func (s *JobRunner) getPodObject(identifier string, labels map[string]string, jo
SecurityContext: &podSecurityContext,
ServiceAccountName: s.podConfig.ServiceAccountName,
NodeSelector: s.podConfig.NodeSelector,
InitContainers: []corev1.Container{
{
Name: ContainerNameHelper,
Image: s.podConfig.helperImage(),
ImagePullPolicy: s.podConfig.PullPolicy,
Command: []string{
"cp",
"/opslevel-runner",
"/mount",
},
VolumeMounts: []corev1.VolumeMount{
{
Name: "shared",
ReadOnly: false,
MountPath: "/mount",
},
},
},
},
InitContainers: initContainers,
Containers: []corev1.Container{
{
Name: ContainerNameJob,
Expand All @@ -238,7 +251,7 @@ func (s *JobRunner) getPodObject(identifier string, labels map[string]string, jo
fmt.Sprintf("sleep %d", s.podConfig.Lifetime),
},
Resources: s.podConfig.Resources,
Env: s.getPodEnv(job.Variables),
Env: s.getPodEnv(job.Variables, opslevel.RunnerJobVariableScopeMain),
SecurityContext: containerSecurityContext,
VolumeMounts: []corev1.VolumeMount{
{
Expand All @@ -251,6 +264,11 @@ func (s *JobRunner) getPodObject(identifier string, labels map[string]string, jo
ReadOnly: true,
MountPath: "/mount",
},
{
Name: "workspace",
ReadOnly: false,
MountPath: s.podConfig.WorkingDir,
},
},
},
},
Expand All @@ -272,6 +290,59 @@ func (s *JobRunner) getPodObject(identifier string, labels map[string]string, jo
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
{
Name: "workspace",
VolumeSource: corev1.VolumeSource{
EmptyDir: &corev1.EmptyDirVolumeSource{},
},
},
},
},
}
}

// getInitContainer assembles a container that runs job.InitCommands before the
// main job container starts. It shares the `workspace` emptyDir with the main
// container at WorkingDir, so anything written here (e.g. a cloned repo) is
// visible to the main container. Only variables scoped to "init" or unscoped
// reach this container — variables scoped to "main" do not.
func (s *JobRunner) getInitContainer(job opslevel.RunnerJob, securityContext *corev1.SecurityContext) corev1.Container {
image := job.InitImage
if image == "" {
image = job.Image
}
workingDirectory := path.Join(s.podConfig.WorkingDir, string(job.Id))
commands := append(
[]string{
fmt.Sprintf("mkdir -p %s", workingDirectory),
fmt.Sprintf("cd %s", workingDirectory),
"set -xv",
},
job.InitCommands...,
)
return corev1.Container{
Name: ContainerNameInit,
Image: image,
ImagePullPolicy: corev1.PullIfNotPresent,
Command: []string{
s.podConfig.Shell,
"-e",
"-c",
strings.Join(commands, ";\n"),
},
Resources: s.podConfig.Resources,
Env: s.getPodEnv(job.Variables, opslevel.RunnerJobVariableScopeInit),
SecurityContext: securityContext,
VolumeMounts: []corev1.VolumeMount{
{
Name: "scripts",
ReadOnly: true,
MountPath: "/opslevel",
},
{
Name: "workspace",
ReadOnly: false,
MountPath: s.podConfig.WorkingDir,
},
},
}
Expand Down
141 changes: 141 additions & 0 deletions src/pkg/k8s_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,5 +173,146 @@ func TestDeleteFunctions_RequireClientset(t *testing.T) {
t.Log("Delete functions correctly handle nil resources")
}

func TestGetPodEnv_FiltersByScope(t *testing.T) {
// Arrange
runner := &JobRunner{logger: zerolog.Nop(), podConfig: &K8SPodConfig{}}
vars := []opslevel.RunnerJobVariable{
{Key: "BOTH", Value: "shared"},
{Key: "INIT_ONLY", Value: "i", Scope: opslevel.RunnerJobVariableScopeInit},
{Key: "MAIN_ONLY", Value: "m", Scope: opslevel.RunnerJobVariableScopeMain},
}

// Act
initEnv := runner.getPodEnv(vars, opslevel.RunnerJobVariableScopeInit)
mainEnv := runner.getPodEnv(vars, opslevel.RunnerJobVariableScopeMain)

// Assert
initKeys := envKeys(initEnv)
mainKeys := envKeys(mainEnv)
autopilot.Equals(t, []string{"BOTH", "INIT_ONLY"}, initKeys)
autopilot.Equals(t, []string{"BOTH", "MAIN_ONLY"}, mainKeys)
}

func TestGetPodObject_NoInitCommands(t *testing.T) {
// Arrange
runner := &JobRunner{
logger: zerolog.Nop(),
podConfig: &K8SPodConfig{
Namespace: "test",
WorkingDir: "/workdir",
Shell: "/bin/sh",
SecurityContext: corev1.PodSecurityContext{},
TerminationGracePeriodSeconds: 30,
},
}
job := opslevel.RunnerJob{Image: "alpine:latest"}

// Act
pod := runner.getPodObject("test-pod", map[string]string{}, job)

// Assert
autopilot.Equals(t, 1, len(pod.Spec.InitContainers))
autopilot.Equals(t, ContainerNameHelper, pod.Spec.InitContainers[0].Name)
// workspace volume is always present, even when no init container runs
autopilot.Assert(t, hasVolume(pod, "workspace"), "workspace volume should be present")
}

func TestGetPodObject_InitContainer(t *testing.T) {
// Arrange
runner := &JobRunner{
logger: zerolog.Nop(),
podConfig: &K8SPodConfig{
Namespace: "test",
WorkingDir: "/workdir",
Shell: "/bin/sh",
SecurityContext: corev1.PodSecurityContext{},
TerminationGracePeriodSeconds: 30,
},
}
job := opslevel.RunnerJob{
Id: "job-1",
Image: "alpine:latest",
InitCommands: []string{"/opslevel/clone-repo ."},
Variables: []opslevel.RunnerJobVariable{
{Key: "REPO_CLONE_URL", Value: "https://token@example/repo.git", Sensitive: true, Scope: opslevel.RunnerJobVariableScopeInit},
{Key: "REPO_URL", Value: "https://example/repo.git"},
{Key: "AI_API_KEY", Value: "secret", Sensitive: true, Scope: opslevel.RunnerJobVariableScopeMain},
},
}

// Act
pod := runner.getPodObject("test-pod", map[string]string{}, job)

// Assert: two init containers (helper, init); init runs second so the
// runner binary is already on the shared mount by the time it boots.
autopilot.Equals(t, 2, len(pod.Spec.InitContainers))
autopilot.Equals(t, ContainerNameHelper, pod.Spec.InitContainers[0].Name)
autopilot.Equals(t, ContainerNameInit, pod.Spec.InitContainers[1].Name)

initContainer := pod.Spec.InitContainers[1]
// Defaults to the job image when InitImage is unset.
autopilot.Equals(t, "alpine:latest", initContainer.Image)
// REPO_CLONE_URL and REPO_URL reach the init container; AI_API_KEY does not.
autopilot.Equals(t, []string{"REPO_CLONE_URL", "REPO_URL"}, envKeys(initContainer.Env))

mainContainer := pod.Spec.Containers[0]
// REPO_CLONE_URL is excluded from the main container — this is the security
// goal of init-container clones.
autopilot.Equals(t, []string{"REPO_URL", "AI_API_KEY"}, envKeys(mainContainer.Env))

// Both init and main mount the workspace RW at WorkingDir.
autopilot.Assert(t, mountIsRW(initContainer.VolumeMounts, "workspace"), "init: workspace should be RW")
autopilot.Assert(t, mountIsRW(mainContainer.VolumeMounts, "workspace"), "main: workspace should be RW")
}

func TestGetPodObject_InitImageOverride(t *testing.T) {
// Arrange
runner := &JobRunner{
logger: zerolog.Nop(),
podConfig: &K8SPodConfig{
Namespace: "test", WorkingDir: "/workdir", Shell: "/bin/sh",
SecurityContext: corev1.PodSecurityContext{}, TerminationGracePeriodSeconds: 30,
},
}
job := opslevel.RunnerJob{
Image: "alpine:latest",
InitImage: "git-tools:latest",
InitCommands: []string{"git --version"},
}

// Act
pod := runner.getPodObject("test-pod", map[string]string{}, job)

// Assert: InitImage takes precedence over Image for the init container.
autopilot.Equals(t, "git-tools:latest", pod.Spec.InitContainers[1].Image)
autopilot.Equals(t, "alpine:latest", pod.Spec.Containers[0].Image)
}

func envKeys(env []corev1.EnvVar) []string {
keys := make([]string, 0, len(env))
for _, e := range env {
keys = append(keys, e.Name)
}
return keys
}

func hasVolume(pod *corev1.Pod, name string) bool {
for _, v := range pod.Spec.Volumes {
if v.Name == name {
return true
}
}
return false
}

func mountIsRW(mounts []corev1.VolumeMount, name string) bool {
for _, m := range mounts {
if m.Name == name {
return !m.ReadOnly
}
}
return false
}

// Suppress unused import warning for policyv1
var _ = policyv1.PodDisruptionBudget{}
Loading