mirror of
https://github.com/morten-olsen/homelab-nuclei-operator.git
synced 2026-02-08 02:16:23 +01:00
fix: run job in operator namespace to avoid permission issues
This commit is contained in:
@@ -77,6 +77,9 @@ type JobReference struct {
|
|||||||
// Name of the Job
|
// Name of the Job
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
|
||||||
|
// Namespace of the Job (may differ from NucleiScan namespace)
|
||||||
|
Namespace string `json:"namespace"`
|
||||||
|
|
||||||
// UID of the Job
|
// UID of the Job
|
||||||
UID string `json:"uid"`
|
UID string `json:"uid"`
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ metadata:
|
|||||||
annotations:
|
annotations:
|
||||||
controller-gen.kubebuilder.io/version: v0.19.0
|
controller-gen.kubebuilder.io/version: v0.19.0
|
||||||
name: nucleiscans.nuclei.homelab.mortenolsen.pro
|
name: nucleiscans.nuclei.homelab.mortenolsen.pro
|
||||||
labels:
|
|
||||||
{{- include "nuclei-operator.labels" . | nindent 4 }}
|
|
||||||
spec:
|
spec:
|
||||||
group: nuclei.homelab.mortenolsen.pro
|
group: nuclei.homelab.mortenolsen.pro
|
||||||
names:
|
names:
|
||||||
@@ -57,6 +55,127 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
description: NucleiScanSpec defines the desired state of NucleiScan
|
description: NucleiScanSpec defines the desired state of NucleiScan
|
||||||
properties:
|
properties:
|
||||||
|
scannerConfig:
|
||||||
|
description: ScannerConfig allows overriding scanner settings for
|
||||||
|
this scan
|
||||||
|
properties:
|
||||||
|
image:
|
||||||
|
description: Image overrides the default scanner image
|
||||||
|
type: string
|
||||||
|
nodeSelector:
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: NodeSelector for scanner pod scheduling
|
||||||
|
type: object
|
||||||
|
resources:
|
||||||
|
description: Resources defines resource requirements for the scanner
|
||||||
|
pod
|
||||||
|
properties:
|
||||||
|
claims:
|
||||||
|
description: |-
|
||||||
|
Claims lists the names of resources, defined in spec.resourceClaims,
|
||||||
|
that are used by this container.
|
||||||
|
|
||||||
|
This field depends on the
|
||||||
|
DynamicResourceAllocation feature gate.
|
||||||
|
|
||||||
|
This field is immutable. It can only be set for containers.
|
||||||
|
items:
|
||||||
|
description: ResourceClaim references one entry in PodSpec.ResourceClaims.
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: |-
|
||||||
|
Name must match the name of one entry in pod.spec.resourceClaims of
|
||||||
|
the Pod where this field is used. It makes that resource available
|
||||||
|
inside a container.
|
||||||
|
type: string
|
||||||
|
request:
|
||||||
|
description: |-
|
||||||
|
Request is the name chosen for a request in the referenced claim.
|
||||||
|
If empty, everything from the claim is made available, otherwise
|
||||||
|
only the result of this request.
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
x-kubernetes-list-map-keys:
|
||||||
|
- name
|
||||||
|
x-kubernetes-list-type: map
|
||||||
|
limits:
|
||||||
|
additionalProperties:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
description: |-
|
||||||
|
Limits describes the maximum amount of compute resources allowed.
|
||||||
|
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||||
|
type: object
|
||||||
|
requests:
|
||||||
|
additionalProperties:
|
||||||
|
anyOf:
|
||||||
|
- type: integer
|
||||||
|
- type: string
|
||||||
|
pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$
|
||||||
|
x-kubernetes-int-or-string: true
|
||||||
|
description: |-
|
||||||
|
Requests describes the minimum amount of compute resources required.
|
||||||
|
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified,
|
||||||
|
otherwise to an implementation-defined value. Requests cannot exceed Limits.
|
||||||
|
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
templateURLs:
|
||||||
|
description: TemplateURLs specifies additional template repositories
|
||||||
|
to clone
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
type: array
|
||||||
|
timeout:
|
||||||
|
description: Timeout overrides the default scan timeout
|
||||||
|
type: string
|
||||||
|
tolerations:
|
||||||
|
description: Tolerations for scanner pod scheduling
|
||||||
|
items:
|
||||||
|
description: |-
|
||||||
|
The pod this Toleration is attached to tolerates any taint that matches
|
||||||
|
the triple <key,value,effect> using the matching operator <operator>.
|
||||||
|
properties:
|
||||||
|
effect:
|
||||||
|
description: |-
|
||||||
|
Effect indicates the taint effect to match. Empty means match all taint effects.
|
||||||
|
When specified, allowed values are NoSchedule, PreferNoSchedule and NoExecute.
|
||||||
|
type: string
|
||||||
|
key:
|
||||||
|
description: |-
|
||||||
|
Key is the taint key that the toleration applies to. Empty means match all taint keys.
|
||||||
|
If the key is empty, operator must be Exists; this combination means to match all values and all keys.
|
||||||
|
type: string
|
||||||
|
operator:
|
||||||
|
description: |-
|
||||||
|
Operator represents a key's relationship to the value.
|
||||||
|
Valid operators are Exists and Equal. Defaults to Equal.
|
||||||
|
Exists is equivalent to wildcard for value, so that a pod can
|
||||||
|
tolerate all taints of a particular category.
|
||||||
|
type: string
|
||||||
|
tolerationSeconds:
|
||||||
|
description: |-
|
||||||
|
TolerationSeconds represents the period of time the toleration (which must be
|
||||||
|
of effect NoExecute, otherwise this field is ignored) tolerates the taint. By default,
|
||||||
|
it is not set, which means tolerate the taint forever (do not evict). Zero and
|
||||||
|
negative values will be treated as 0 (evict immediately) by the system.
|
||||||
|
format: int64
|
||||||
|
type: integer
|
||||||
|
value:
|
||||||
|
description: |-
|
||||||
|
Value is the taint value the toleration matches to.
|
||||||
|
If the operator is Exists, the value should be empty, otherwise just a regular string.
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
schedule:
|
schedule:
|
||||||
description: |-
|
description: |-
|
||||||
Schedule for periodic rescanning in cron format
|
Schedule for periodic rescanning in cron format
|
||||||
@@ -64,12 +183,17 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
severity:
|
severity:
|
||||||
description: Severity filters scan results by severity level
|
description: Severity filters scan results by severity level
|
||||||
|
enum:
|
||||||
|
- info
|
||||||
|
- low
|
||||||
|
- medium
|
||||||
|
- high
|
||||||
|
- critical
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
type: array
|
type: array
|
||||||
sourceRef:
|
sourceRef:
|
||||||
description:
|
description: SourceRef references the Ingress or VirtualService being
|
||||||
SourceRef references the Ingress or VirtualService being
|
|
||||||
scanned
|
scanned
|
||||||
properties:
|
properties:
|
||||||
apiVersion:
|
apiVersion:
|
||||||
@@ -101,8 +225,7 @@ spec:
|
|||||||
description: Suspend prevents scheduled scans from running
|
description: Suspend prevents scheduled scans from running
|
||||||
type: boolean
|
type: boolean
|
||||||
targets:
|
targets:
|
||||||
description:
|
description: Targets is the list of URLs to scan, extracted from the
|
||||||
Targets is the list of URLs to scan, extracted from the
|
|
||||||
source resource
|
source resource
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -129,8 +252,7 @@ spec:
|
|||||||
conditions:
|
conditions:
|
||||||
description: Conditions represent the latest available observations
|
description: Conditions represent the latest available observations
|
||||||
items:
|
items:
|
||||||
description:
|
description: Condition contains details for one aspect of the current
|
||||||
Condition contains details for one aspect of the current
|
|
||||||
state of this API Resource.
|
state of this API Resource.
|
||||||
properties:
|
properties:
|
||||||
lastTransitionTime:
|
lastTransitionTime:
|
||||||
@@ -198,8 +320,7 @@ spec:
|
|||||||
description: Description provides details about the finding
|
description: Description provides details about the finding
|
||||||
type: string
|
type: string
|
||||||
extractedResults:
|
extractedResults:
|
||||||
description:
|
description: ExtractedResults contains any data extracted by
|
||||||
ExtractedResults contains any data extracted by
|
|
||||||
the template
|
the template
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -208,8 +329,7 @@ spec:
|
|||||||
description: Host that was scanned
|
description: Host that was scanned
|
||||||
type: string
|
type: string
|
||||||
matchedAt:
|
matchedAt:
|
||||||
description:
|
description: MatchedAt is the specific URL or endpoint where
|
||||||
MatchedAt is the specific URL or endpoint where
|
|
||||||
the issue was found
|
the issue was found
|
||||||
type: string
|
type: string
|
||||||
metadata:
|
metadata:
|
||||||
@@ -217,8 +337,7 @@ spec:
|
|||||||
type: object
|
type: object
|
||||||
x-kubernetes-preserve-unknown-fields: true
|
x-kubernetes-preserve-unknown-fields: true
|
||||||
reference:
|
reference:
|
||||||
description:
|
description: Reference contains URLs to additional information
|
||||||
Reference contains URLs to additional information
|
|
||||||
about the finding
|
about the finding
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
@@ -251,12 +370,36 @@ spec:
|
|||||||
- timestamp
|
- timestamp
|
||||||
type: object
|
type: object
|
||||||
type: array
|
type: array
|
||||||
|
jobRef:
|
||||||
|
description: JobRef references the current or last scanner job
|
||||||
|
properties:
|
||||||
|
name:
|
||||||
|
description: Name of the Job
|
||||||
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: Namespace of the Job (may differ from NucleiScan
|
||||||
|
namespace)
|
||||||
|
type: string
|
||||||
|
podName:
|
||||||
|
description: PodName is the name of the scanner pod (for log retrieval)
|
||||||
|
type: string
|
||||||
|
startTime:
|
||||||
|
description: StartTime when the job was created
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
|
uid:
|
||||||
|
description: UID of the Job
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- name
|
||||||
|
- namespace
|
||||||
|
- uid
|
||||||
|
type: object
|
||||||
lastError:
|
lastError:
|
||||||
description: LastError contains the error message if the scan failed
|
description: LastError contains the error message if the scan failed
|
||||||
type: string
|
type: string
|
||||||
lastRetryTime:
|
lastRetryTime:
|
||||||
description:
|
description: LastRetryTime is when the last availability check retry
|
||||||
LastRetryTime is when the last availability check retry
|
|
||||||
occurred
|
occurred
|
||||||
format: date-time
|
format: date-time
|
||||||
type: string
|
type: string
|
||||||
@@ -265,14 +408,12 @@ spec:
|
|||||||
format: date-time
|
format: date-time
|
||||||
type: string
|
type: string
|
||||||
nextScheduledTime:
|
nextScheduledTime:
|
||||||
description:
|
description: NextScheduledTime is when the next scheduled scan will
|
||||||
NextScheduledTime is when the next scheduled scan will
|
|
||||||
run
|
run
|
||||||
format: date-time
|
format: date-time
|
||||||
type: string
|
type: string
|
||||||
observedGeneration:
|
observedGeneration:
|
||||||
description:
|
description: ObservedGeneration is the generation observed by the
|
||||||
ObservedGeneration is the generation observed by the
|
|
||||||
controller
|
controller
|
||||||
format: int64
|
format: int64
|
||||||
type: integer
|
type: integer
|
||||||
@@ -289,6 +430,11 @@ spec:
|
|||||||
RetryCount tracks the number of consecutive availability check retries
|
RetryCount tracks the number of consecutive availability check retries
|
||||||
Used for exponential backoff when waiting for targets
|
Used for exponential backoff when waiting for targets
|
||||||
type: integer
|
type: integer
|
||||||
|
scanStartTime:
|
||||||
|
description: ScanStartTime is when the scanner pod actually started
|
||||||
|
scanning
|
||||||
|
format: date-time
|
||||||
|
type: string
|
||||||
summary:
|
summary:
|
||||||
description: Summary provides aggregated scan statistics
|
description: Summary provides aggregated scan statistics
|
||||||
properties:
|
properties:
|
||||||
@@ -299,13 +445,11 @@ spec:
|
|||||||
findingsBySeverity:
|
findingsBySeverity:
|
||||||
additionalProperties:
|
additionalProperties:
|
||||||
type: integer
|
type: integer
|
||||||
description:
|
description: FindingsBySeverity breaks down findings by severity
|
||||||
FindingsBySeverity breaks down findings by severity
|
|
||||||
level
|
level
|
||||||
type: object
|
type: object
|
||||||
targetsScanned:
|
targetsScanned:
|
||||||
description:
|
description: TargetsScanned is the number of targets that were
|
||||||
TargetsScanned is the number of targets that were
|
|
||||||
scanned
|
scanned
|
||||||
type: integer
|
type: integer
|
||||||
totalFindings:
|
totalFindings:
|
||||||
|
|||||||
@@ -70,6 +70,8 @@ spec:
|
|||||||
value: {{ .Values.scanner.ttlAfterFinished | quote }}
|
value: {{ .Values.scanner.ttlAfterFinished | quote }}
|
||||||
- name: SCANNER_SERVICE_ACCOUNT
|
- name: SCANNER_SERVICE_ACCOUNT
|
||||||
value: {{ include "nuclei-operator.fullname" . }}-scanner
|
value: {{ include "nuclei-operator.fullname" . }}-scanner
|
||||||
|
- name: OPERATOR_NAMESPACE
|
||||||
|
value: {{ .Release.Namespace | quote }}
|
||||||
{{- if .Values.scanner.defaultTemplates }}
|
{{- if .Values.scanner.defaultTemplates }}
|
||||||
- name: DEFAULT_TEMPLATES
|
- name: DEFAULT_TEMPLATES
|
||||||
value: {{ join "," .Values.scanner.defaultTemplates | quote }}
|
value: {{ join "," .Values.scanner.defaultTemplates | quote }}
|
||||||
|
|||||||
11
cmd/main.go
11
cmd/main.go
@@ -241,6 +241,16 @@ func main() {
|
|||||||
scannerServiceAccount = "nuclei-scanner"
|
scannerServiceAccount = "nuclei-scanner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
operatorNamespace := os.Getenv("OPERATOR_NAMESPACE")
|
||||||
|
if operatorNamespace == "" {
|
||||||
|
// Try to read from the downward API file
|
||||||
|
if data, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/namespace"); err == nil {
|
||||||
|
operatorNamespace = string(data)
|
||||||
|
} else {
|
||||||
|
operatorNamespace = "nuclei-operator-system"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
defaultTemplates := []string{}
|
defaultTemplates := []string{}
|
||||||
if v := os.Getenv("DEFAULT_TEMPLATES"); v != "" {
|
if v := os.Getenv("DEFAULT_TEMPLATES"); v != "" {
|
||||||
defaultTemplates = strings.Split(v, ",")
|
defaultTemplates = strings.Split(v, ",")
|
||||||
@@ -259,6 +269,7 @@ func main() {
|
|||||||
BackoffLimit: 2,
|
BackoffLimit: 2,
|
||||||
MaxConcurrent: maxConcurrentScans,
|
MaxConcurrent: maxConcurrentScans,
|
||||||
ServiceAccountName: scannerServiceAccount,
|
ServiceAccountName: scannerServiceAccount,
|
||||||
|
OperatorNamespace: operatorNamespace,
|
||||||
DefaultResources: jobmanager.DefaultConfig().DefaultResources,
|
DefaultResources: jobmanager.DefaultConfig().DefaultResources,
|
||||||
DefaultTemplates: defaultTemplates,
|
DefaultTemplates: defaultTemplates,
|
||||||
DefaultSeverity: defaultSeverity,
|
DefaultSeverity: defaultSeverity,
|
||||||
|
|||||||
@@ -376,6 +376,10 @@ spec:
|
|||||||
name:
|
name:
|
||||||
description: Name of the Job
|
description: Name of the Job
|
||||||
type: string
|
type: string
|
||||||
|
namespace:
|
||||||
|
description: Namespace of the Job (may differ from NucleiScan
|
||||||
|
namespace)
|
||||||
|
type: string
|
||||||
podName:
|
podName:
|
||||||
description: PodName is the name of the scanner pod (for log retrieval)
|
description: PodName is the name of the scanner pod (for log retrieval)
|
||||||
type: string
|
type: string
|
||||||
@@ -388,6 +392,7 @@ spec:
|
|||||||
type: string
|
type: string
|
||||||
required:
|
required:
|
||||||
- name
|
- name
|
||||||
|
- namespace
|
||||||
- uid
|
- uid
|
||||||
type: object
|
type: object
|
||||||
lastError:
|
lastError:
|
||||||
|
|||||||
@@ -257,8 +257,13 @@ func (r *NucleiScanReconciler) handleDeletion(ctx context.Context, nucleiScan *n
|
|||||||
|
|
||||||
// Clean up any running scanner job
|
// Clean up any running scanner job
|
||||||
if nucleiScan.Status.JobRef != nil {
|
if nucleiScan.Status.JobRef != nil {
|
||||||
log.Info("Deleting scanner job", "job", nucleiScan.Status.JobRef.Name)
|
jobNamespace := nucleiScan.Status.JobRef.Namespace
|
||||||
if err := r.JobManager.DeleteJob(ctx, nucleiScan.Status.JobRef.Name, nucleiScan.Namespace); err != nil {
|
if jobNamespace == "" {
|
||||||
|
// Fallback for backwards compatibility
|
||||||
|
jobNamespace = nucleiScan.Namespace
|
||||||
|
}
|
||||||
|
log.Info("Deleting scanner job", "job", nucleiScan.Status.JobRef.Name, "namespace", jobNamespace)
|
||||||
|
if err := r.JobManager.DeleteJob(ctx, nucleiScan.Status.JobRef.Name, jobNamespace); err != nil {
|
||||||
if !apierrors.IsNotFound(err) {
|
if !apierrors.IsNotFound(err) {
|
||||||
log.Error(err, "Failed to delete scanner job", "job", nucleiScan.Status.JobRef.Name)
|
log.Error(err, "Failed to delete scanner job", "job", nucleiScan.Status.JobRef.Name)
|
||||||
}
|
}
|
||||||
@@ -324,6 +329,7 @@ func (r *NucleiScanReconciler) handlePendingPhase(ctx context.Context, nucleiSca
|
|||||||
nucleiScan.Status.Phase = nucleiv1alpha1.ScanPhaseRunning
|
nucleiScan.Status.Phase = nucleiv1alpha1.ScanPhaseRunning
|
||||||
nucleiScan.Status.JobRef = &nucleiv1alpha1.JobReference{
|
nucleiScan.Status.JobRef = &nucleiv1alpha1.JobReference{
|
||||||
Name: job.Name,
|
Name: job.Name,
|
||||||
|
Namespace: job.Namespace,
|
||||||
UID: string(job.UID),
|
UID: string(job.UID),
|
||||||
StartTime: &now,
|
StartTime: &now,
|
||||||
}
|
}
|
||||||
@@ -401,8 +407,13 @@ func (r *NucleiScanReconciler) handleRunningPhase(ctx context.Context, nucleiSca
|
|||||||
return ctrl.Result{Requeue: true}, nil
|
return ctrl.Result{Requeue: true}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the job
|
// Get the job - use namespace from JobRef (may be different from scan namespace)
|
||||||
job, err := r.JobManager.GetJob(ctx, nucleiScan.Status.JobRef.Name, nucleiScan.Namespace)
|
jobNamespace := nucleiScan.Status.JobRef.Namespace
|
||||||
|
if jobNamespace == "" {
|
||||||
|
// Fallback for backwards compatibility
|
||||||
|
jobNamespace = nucleiScan.Namespace
|
||||||
|
}
|
||||||
|
job, err := r.JobManager.GetJob(ctx, nucleiScan.Status.JobRef.Name, jobNamespace)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if apierrors.IsNotFound(err) {
|
if apierrors.IsNotFound(err) {
|
||||||
logger.Info("Scanner job not found, resetting to Pending")
|
logger.Info("Scanner job not found, resetting to Pending")
|
||||||
|
|||||||
@@ -82,6 +82,9 @@ type Config struct {
|
|||||||
// ServiceAccountName is the service account to use for scanner pods
|
// ServiceAccountName is the service account to use for scanner pods
|
||||||
ServiceAccountName string
|
ServiceAccountName string
|
||||||
|
|
||||||
|
// OperatorNamespace is the namespace where the operator runs and where scanner jobs will be created
|
||||||
|
OperatorNamespace string
|
||||||
|
|
||||||
// DefaultResources are the default resource requirements for scanner pods
|
// DefaultResources are the default resource requirements for scanner pods
|
||||||
DefaultResources corev1.ResourceRequirements
|
DefaultResources corev1.ResourceRequirements
|
||||||
|
|
||||||
@@ -101,6 +104,7 @@ func DefaultConfig() Config {
|
|||||||
BackoffLimit: DefaultBackoffLimit,
|
BackoffLimit: DefaultBackoffLimit,
|
||||||
MaxConcurrent: 5,
|
MaxConcurrent: 5,
|
||||||
ServiceAccountName: "nuclei-scanner",
|
ServiceAccountName: "nuclei-scanner",
|
||||||
|
OperatorNamespace: "nuclei-operator-system",
|
||||||
DefaultResources: corev1.ResourceRequirements{
|
DefaultResources: corev1.ResourceRequirements{
|
||||||
Requests: corev1.ResourceList{
|
Requests: corev1.ResourceList{
|
||||||
corev1.ResourceCPU: resource.MustParse("100m"),
|
corev1.ResourceCPU: resource.MustParse("100m"),
|
||||||
@@ -136,14 +140,22 @@ func (m *JobManager) CreateScanJob(ctx context.Context, scan *nucleiv1alpha1.Nuc
|
|||||||
|
|
||||||
job := m.buildJob(scan)
|
job := m.buildJob(scan)
|
||||||
|
|
||||||
// Set owner reference so the job is garbage collected when the scan is deleted
|
// Only set owner reference if the job is in the same namespace as the scan
|
||||||
|
// Cross-namespace owner references are not allowed in Kubernetes
|
||||||
|
if job.Namespace == scan.Namespace {
|
||||||
if err := controllerutil.SetControllerReference(scan, job, m.Scheme); err != nil {
|
if err := controllerutil.SetControllerReference(scan, job, m.Scheme); err != nil {
|
||||||
return nil, fmt.Errorf("failed to set controller reference: %w", err)
|
return nil, fmt.Errorf("failed to set controller reference: %w", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
// When job is in a different namespace (operator namespace), we rely on:
|
||||||
|
// 1. TTLSecondsAfterFinished for automatic cleanup of completed jobs
|
||||||
|
// 2. Labels (LabelScanName, LabelScanNamespace) to track which scan the job belongs to
|
||||||
|
// 3. CleanupOrphanedJobs to clean up jobs whose scans no longer exist
|
||||||
|
|
||||||
logger.Info("Creating scanner job",
|
logger.Info("Creating scanner job",
|
||||||
"job", job.Name,
|
"job", job.Name,
|
||||||
"namespace", job.Namespace,
|
"jobNamespace", job.Namespace,
|
||||||
|
"scanNamespace", scan.Namespace,
|
||||||
"image", job.Spec.Template.Spec.Containers[0].Image,
|
"image", job.Spec.Template.Spec.Containers[0].Image,
|
||||||
"targets", len(scan.Spec.Targets))
|
"targets", len(scan.Spec.Targets))
|
||||||
|
|
||||||
@@ -277,15 +289,42 @@ func (m *JobManager) CleanupOrphanedJobs(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for _, job := range jobList.Items {
|
for _, job := range jobList.Items {
|
||||||
// Check if owner reference exists and the owner still exists
|
// Check if the associated NucleiScan still exists using labels
|
||||||
ownerRef := metav1.GetControllerOf(&job)
|
scanName := job.Labels[LabelScanName]
|
||||||
if ownerRef == nil {
|
scanNamespace := job.Labels[LabelScanNamespace]
|
||||||
logger.Info("Deleting orphaned job without owner", "job", job.Name, "namespace", job.Namespace)
|
|
||||||
|
if scanName != "" && scanNamespace != "" {
|
||||||
|
// Try to get the associated NucleiScan
|
||||||
|
scan := &nucleiv1alpha1.NucleiScan{}
|
||||||
|
err := m.Get(ctx, types.NamespacedName{Name: scanName, Namespace: scanNamespace}, scan)
|
||||||
|
if err != nil {
|
||||||
|
if apierrors.IsNotFound(err) {
|
||||||
|
// The scan no longer exists - delete the job
|
||||||
|
logger.Info("Deleting orphaned job (scan not found)",
|
||||||
|
"job", job.Name,
|
||||||
|
"namespace", job.Namespace,
|
||||||
|
"scanName", scanName,
|
||||||
|
"scanNamespace", scanNamespace)
|
||||||
if err := m.DeleteJob(ctx, job.Name, job.Namespace); err != nil && !apierrors.IsNotFound(err) {
|
if err := m.DeleteJob(ctx, job.Name, job.Namespace); err != nil && !apierrors.IsNotFound(err) {
|
||||||
logger.Error(err, "Failed to delete orphaned job", "job", job.Name)
|
logger.Error(err, "Failed to delete orphaned job", "job", job.Name)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
// Other error - log and continue
|
||||||
|
logger.Error(err, "Failed to check if scan exists", "scanName", scanName, "scanNamespace", scanNamespace)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Job doesn't have proper labels - check owner reference as fallback
|
||||||
|
ownerRef := metav1.GetControllerOf(&job)
|
||||||
|
if ownerRef == nil {
|
||||||
|
logger.Info("Deleting orphaned job without owner or labels", "job", job.Name, "namespace", job.Namespace)
|
||||||
|
if err := m.DeleteJob(ctx, job.Name, job.Namespace); err != nil && !apierrors.IsNotFound(err) {
|
||||||
|
logger.Error(err, "Failed to delete orphaned job", "job", job.Name)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if the job is stuck (running longer than 2x the timeout)
|
// Check if the job is stuck (running longer than 2x the timeout)
|
||||||
if job.Status.StartTime != nil {
|
if job.Status.StartTime != nil {
|
||||||
@@ -305,12 +344,18 @@ func (m *JobManager) CleanupOrphanedJobs(ctx context.Context) error {
|
|||||||
|
|
||||||
// buildJob creates a Job specification for the given NucleiScan
|
// buildJob creates a Job specification for the given NucleiScan
|
||||||
func (m *JobManager) buildJob(scan *nucleiv1alpha1.NucleiScan) *batchv1.Job {
|
func (m *JobManager) buildJob(scan *nucleiv1alpha1.NucleiScan) *batchv1.Job {
|
||||||
// Generate a unique job name
|
// Generate a unique job name that includes the scan namespace to avoid collisions
|
||||||
jobName := fmt.Sprintf("nucleiscan-%s-%d", scan.Name, time.Now().Unix())
|
jobName := fmt.Sprintf("nucleiscan-%s-%s-%d", scan.Namespace, scan.Name, time.Now().Unix())
|
||||||
if len(jobName) > 63 {
|
if len(jobName) > 63 {
|
||||||
jobName = jobName[:63]
|
jobName = jobName[:63]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Determine the namespace for the job - use operator namespace if configured
|
||||||
|
jobNamespace := m.Config.OperatorNamespace
|
||||||
|
if jobNamespace == "" {
|
||||||
|
jobNamespace = scan.Namespace
|
||||||
|
}
|
||||||
|
|
||||||
// Determine the scanner image
|
// Determine the scanner image
|
||||||
image := m.Config.ScannerImage
|
image := m.Config.ScannerImage
|
||||||
if scan.Spec.ScannerConfig != nil && scan.Spec.ScannerConfig.Image != "" {
|
if scan.Spec.ScannerConfig != nil && scan.Spec.ScannerConfig.Image != "" {
|
||||||
@@ -360,7 +405,7 @@ func (m *JobManager) buildJob(scan *nucleiv1alpha1.NucleiScan) *batchv1.Job {
|
|||||||
job := &batchv1.Job{
|
job := &batchv1.Job{
|
||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: jobName,
|
Name: jobName,
|
||||||
Namespace: scan.Namespace,
|
Namespace: jobNamespace,
|
||||||
Labels: labels,
|
Labels: labels,
|
||||||
},
|
},
|
||||||
Spec: batchv1.JobSpec{
|
Spec: batchv1.JobSpec{
|
||||||
|
|||||||
@@ -43,14 +43,22 @@ func TestBuildJob(t *testing.T) {
|
|||||||
|
|
||||||
job := manager.buildJob(scan)
|
job := manager.buildJob(scan)
|
||||||
|
|
||||||
// Verify job name prefix
|
// Verify job name prefix - should include scan namespace to avoid collisions
|
||||||
if len(job.Name) == 0 {
|
if len(job.Name) == 0 {
|
||||||
t.Error("Job name should not be empty")
|
t.Error("Job name should not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify namespace
|
// Verify namespace - job should be created in operator namespace
|
||||||
if job.Namespace != "default" {
|
if job.Namespace != config.OperatorNamespace {
|
||||||
t.Errorf("Expected namespace 'default', got '%s'", job.Namespace)
|
t.Errorf("Expected namespace '%s', got '%s'", config.OperatorNamespace, job.Namespace)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify scan labels are set correctly for cross-namespace tracking
|
||||||
|
if job.Labels[LabelScanName] != scan.Name {
|
||||||
|
t.Errorf("Expected scan name label '%s', got '%s'", scan.Name, job.Labels[LabelScanName])
|
||||||
|
}
|
||||||
|
if job.Labels[LabelScanNamespace] != scan.Namespace {
|
||||||
|
t.Errorf("Expected scan namespace label '%s', got '%s'", scan.Namespace, job.Labels[LabelScanNamespace])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify labels
|
// Verify labels
|
||||||
@@ -115,3 +123,29 @@ func TestBuildJobWithCustomConfig(t *testing.T) {
|
|||||||
t.Errorf("Expected deadline %d, got %d", expectedDeadline, *job.Spec.ActiveDeadlineSeconds)
|
t.Errorf("Expected deadline %d, got %d", expectedDeadline, *job.Spec.ActiveDeadlineSeconds)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildJobInSameNamespace(t *testing.T) {
|
||||||
|
config := DefaultConfig()
|
||||||
|
// Clear operator namespace to test same-namespace behavior
|
||||||
|
config.OperatorNamespace = ""
|
||||||
|
manager := &JobManager{
|
||||||
|
Config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
scan := &nucleiv1alpha1.NucleiScan{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test-scan",
|
||||||
|
Namespace: "my-namespace",
|
||||||
|
},
|
||||||
|
Spec: nucleiv1alpha1.NucleiScanSpec{
|
||||||
|
Targets: []string{"https://example.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
job := manager.buildJob(scan)
|
||||||
|
|
||||||
|
// Verify namespace - when operator namespace is empty, job should be in scan's namespace
|
||||||
|
if job.Namespace != scan.Namespace {
|
||||||
|
t.Errorf("Expected namespace '%s', got '%s'", scan.Namespace, job.Namespace)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user