secureCodeBox/secureCodeBox-v2-alpha

View on GitHub
operator/controllers/execution/scans/hook_reconciler.go

Summary

Maintainability
D
2 days
Test Coverage
File `hook_reconciler.go` has 378 lines of code (exceeds 300 allowed). Consider refactoring.
package scancontrollers
 
import (
"context"
"fmt"
 
executionv1 "github.com/secureCodeBox/secureCodeBox-v2/operator/apis/execution/v1"
util "github.com/secureCodeBox/secureCodeBox-v2/operator/utils"
batch "k8s.io/api/batch/v1"
corev1 "k8s.io/api/core/v1"
rbacv1 "k8s.io/api/rbac/v1"
resource "k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
)
 
func (r *ScanReconciler) setHookStatus(scan *executionv1.Scan) error {
// Set (pending) Hook status on the scan
ctx := context.Background()
var scanCompletionHooks executionv1.ScanCompletionHookList
 
if err := r.List(ctx, &scanCompletionHooks, client.InNamespace(scan.Namespace)); err != nil {
r.Log.V(7).Info("Unable to fetch ScanCompletionHooks")
return err
}
 
r.Log.Info("Found ScanCompletionHooks", "ScanCompletionHooks", len(scanCompletionHooks.Items))
 
readAndWriteHooks := []executionv1.ScanCompletionHook{}
// filter all ReadAndWriteHooks in the scamCompletionHooks list
for _, hook := range scanCompletionHooks.Items {
if hook.Spec.Type == executionv1.ReadAndWrite {
readAndWriteHooks = append(readAndWriteHooks, hook)
}
}
 
r.Log.Info("Found ReadAndWriteHooks", "ReadAndWriteHooks", len(readAndWriteHooks))
 
hookStatus := []executionv1.HookStatus{}
 
for _, hook := range readAndWriteHooks {
hookStatus = append(hookStatus, executionv1.HookStatus{
HookName: hook.Name,
State: executionv1.Pending,
})
}
 
scan.Status.State = "ReadAndWriteHookProcessing"
scan.Status.ReadAndWriteHookStatus = hookStatus
 
if err := r.Status().Update(ctx, scan); err != nil {
r.Log.Error(err, "unable to update Scan status")
return err
}
 
return nil
}
 
Method `ScanReconciler.executeReadAndWriteHooks` has 101 lines of code (exceeds 50 allowed). Consider refactoring.
Method `ScanReconciler.executeReadAndWriteHooks` has a Cognitive Complexity of 36 (exceeds 20 allowed). Consider refactoring.
Method `ScanReconciler.executeReadAndWriteHooks` has 14 return statements (exceeds 4 allowed).
func (r *ScanReconciler) executeReadAndWriteHooks(scan *executionv1.Scan) error {
// Get the first Hook Status which is not completed.
ctx := context.Background()
var nonCompletedHook *executionv1.HookStatus
 
for _, hook := range scan.Status.ReadAndWriteHookStatus {
if hook.State != executionv1.Completed {
nonCompletedHook = &hook
break
}
}
 
// If nil then all hooks are done
if nonCompletedHook == nil {
scan.Status.State = "ReadAndWriteHookCompleted"
if err := r.Status().Update(ctx, scan); err != nil {
r.Log.Error(err, "unable to update Scan status")
return err
}
return nil
}
 
switch nonCompletedHook.State {
case executionv1.Pending:
rawFileURL, err := r.PresignedGetURL(scan.UID, scan.Status.RawResultFile, defaultPresignDuration)
if err != nil {
return err
}
findingsFileURL, err := r.PresignedGetURL(scan.UID, "findings.json", defaultPresignDuration)
if err != nil {
return err
}
 
rawFileUploadURL, err := r.PresignedPutURL(scan.UID, scan.Status.RawResultFile, defaultPresignDuration)
if err != nil {
return err
}
findingsUploadURL, err := r.PresignedPutURL(scan.UID, "findings.json", defaultPresignDuration)
if err != nil {
return err
}
 
var hook executionv1.ScanCompletionHook
if err := r.Get(ctx, types.NamespacedName{Name: nonCompletedHook.HookName, Namespace: scan.Namespace}, &hook); err != nil {
r.Log.Error(err, "Failed to get ReadAndWrite Hook for HookStatus")
return err
}
 
jobs, err := r.getJobsForScan(scan, client.MatchingLabels{
"securecodebox.io/job-type": "read-and-write-hook",
"securecodebox.io/hook-name": nonCompletedHook.HookName,
})
if err != nil {
return err
}
if len(jobs.Items) > 0 {
// Job already exists
return nil
}
 
jobName, err := r.createJobForHook(
&hook,
scan,
[]string{
rawFileURL,
findingsFileURL,
rawFileUploadURL,
findingsUploadURL,
},
)
 
// Update the currently executed hook status to "InProgress"
err = r.updateHookStatus(scan, executionv1.HookStatus{
HookName: nonCompletedHook.HookName,
JobName: jobName,
State: executionv1.InProgress,
})
return err
case executionv1.InProgress:
jobStatus, err := r.checkIfJobIsCompleted(scan, client.MatchingLabels{
"securecodebox.io/job-type": "read-and-write-hook",
"securecodebox.io/hook-name": nonCompletedHook.HookName,
})
if err != nil {
r.Log.Error(err, "Failed to check job status for ReadAndWrite Hook")
return err
}
switch jobStatus {
case completed:
// Job is completed => set current Hook to completed
err = r.updateHookStatus(scan, executionv1.HookStatus{
HookName: nonCompletedHook.HookName,
JobName: nonCompletedHook.JobName,
State: executionv1.Completed,
})
return err
case incomplete:
// Still waiting for job to finish
return nil
case failed:
for i, hookStatus := range scan.Status.ReadAndWriteHookStatus {
if hookStatus.HookName == nonCompletedHook.HookName {
scan.Status.ReadAndWriteHookStatus[i].State = executionv1.Failed
} else if hookStatus.State == executionv1.Pending {
scan.Status.ReadAndWriteHookStatus[i].State = executionv1.Cancelled
}
}
scan.Status.State = "Errored"
scan.Status.ErrorDescription = fmt.Sprintf("Failed to execute ReadAndWrite Hook '%s' in job '%s'. Check the logs of the hook for more information.", nonCompletedHook.HookName, nonCompletedHook.JobName)
if err := r.Status().Update(ctx, scan); err != nil {
r.Log.Error(err, "unable to update Scan status")
return err
}
}
}
 
return nil
}
 
func containsJobForHook(jobs *batch.JobList, hook executionv1.ScanCompletionHook) bool {
if len(jobs.Items) == 0 {
return false
}
 
for _, job := range jobs.Items {
if job.ObjectMeta.Labels["securecodebox.io/hook-name"] == hook.Name {
return true
}
}
 
return false
}
 
Method `ScanReconciler.startReadOnlyHooks` has 64 lines of code (exceeds 50 allowed). Consider refactoring.
Method `ScanReconciler.startReadOnlyHooks` has 9 return statements (exceeds 4 allowed).
func (r *ScanReconciler) startReadOnlyHooks(scan *executionv1.Scan) error {
ctx := context.Background()
 
var scanCompletionHooks executionv1.ScanCompletionHookList
 
if err := r.List(ctx, &scanCompletionHooks, client.InNamespace(scan.Namespace)); err != nil {
r.Log.V(7).Info("Unable to fetch ScanCompletionHooks")
return err
}
 
r.Log.Info("Found ScanCompletionHooks", "ScanCompletionHooks", len(scanCompletionHooks.Items))
 
readOnlyHooks := []executionv1.ScanCompletionHook{}
// filter all ReadOnlyHooks in the scamCompletionHooks list
for _, hook := range scanCompletionHooks.Items {
if hook.Spec.Type == executionv1.ReadOnly {
readOnlyHooks = append(readOnlyHooks, hook)
}
}
 
r.Log.Info("Found ReadOnlyHooks", "ReadOnlyHooks", len(readOnlyHooks))
 
// If the readOnlyHooks list is empty, nothing more to do
if len(readOnlyHooks) == 0 {
r.Log.Info("Marked scan as done as without running ReadOnly hooks as non were configured", "ScanName", scan.Name)
scan.Status.State = "Done"
var now metav1.Time = metav1.Now()
scan.Status.FinishedAt = &now
if err := r.Status().Update(ctx, scan); err != nil {
r.Log.Error(err, "Unable to update Scan status")
return err
}
return nil
}
 
// Get all read-only-hooks for scan to later check that they weren't already created
jobs, err := r.getJobsForScan(scan, client.MatchingLabels{
"securecodebox.io/job-type": "read-only-hook",
})
if err != nil {
return err
}
 
for _, hook := range readOnlyHooks {
// Check if hook was already executed
if containsJobForHook(jobs, hook) == true {
r.Log.V(4).Info("Skipping creation of job for hook '%s' as it already exists", hook.Name)
// Job was already created
continue
}
 
rawFileURL, err := r.PresignedGetURL(scan.UID, scan.Status.RawResultFile, defaultPresignDuration)
if err != nil {
return err
}
findingsFileURL, err := r.PresignedGetURL(scan.UID, "findings.json", defaultPresignDuration)
if err != nil {
return err
}
 
jobName, err := r.createJobForHook(
&hook,
scan,
[]string{
rawFileURL,
findingsFileURL,
},
)
if err != nil {
r.Log.Error(err, "Unable to create Job for ReadOnlyHook", "job", jobName)
return err
}
}
scan.Status.State = "ReadOnlyHookProcessing"
if err := r.Status().Update(ctx, scan); err != nil {
r.Log.Error(err, "Unable to update Scan status")
return err
}
r.Log.Info("Started ReadOnlyHook", "ReadOnlyHookCount", len(readOnlyHooks))
return nil
}
 
func (r *ScanReconciler) checkIfReadOnlyHookIsCompleted(scan *executionv1.Scan) error {
ctx := context.Background()
readOnlyHookCompletion, err := r.checkIfJobIsCompleted(scan, client.MatchingLabels{"securecodebox.io/job-type": "read-only-hook"})
if err != nil {
return err
}
 
if readOnlyHookCompletion == completed {
r.Log.V(7).Info("All ReadOnlyHooks have completed")
scan.Status.State = "Done"
var now metav1.Time = metav1.Now()
scan.Status.FinishedAt = &now
if err := r.Status().Update(ctx, scan); err != nil {
r.Log.Error(err, "Unable to update Scan status")
return err
}
} else if readOnlyHookCompletion == failed {
r.Log.Info("At least one ReadOnlyHook failed")
scan.Status.State = "Errored"
scan.Status.ErrorDescription = "At least one ReadOnlyHook failed, check the hooks kubernetes jobs related to the scan for more details."
if err := r.Status().Update(ctx, scan); err != nil {
r.Log.Error(err, "Unable to update Scan status")
return err
}
}
 
// ReadOnlyHook(s) are still running. At least some of them are.
// Waiting until all are done.
return nil
}
 
Method `ScanReconciler.createJobForHook` has 112 lines of code (exceeds 50 allowed). Consider refactoring.
func (r *ScanReconciler) createJobForHook(hook *executionv1.ScanCompletionHook, scan *executionv1.Scan, cliArgs []string) (string, error) {
ctx := context.Background()
 
serviceAccountName := "scan-completion-hook"
if hook.Spec.ServiceAccountName != nil {
// Hook uses a custom ServiceAccount
serviceAccountName = *hook.Spec.ServiceAccountName
} else {
// Check and create a serviceAccount for the hook in its namespace, if it doesn't already exist.
rules := []rbacv1.PolicyRule{
{
APIGroups: []string{"execution.securecodebox.io"},
Resources: []string{"scans"},
Verbs: []string{"get"},
},
{
APIGroups: []string{"execution.securecodebox.io"},
Resources: []string{"scans/status"},
Verbs: []string{"get", "patch"},
},
}
 
r.ensureServiceAccountExists(
hook.Namespace,
serviceAccountName,
"ScanCompletionHooks need to access the current scan to view where its results are stored",
rules,
)
}
 
standardEnvVars := []corev1.EnvVar{
{
Name: "NAMESPACE",
ValueFrom: &corev1.EnvVarSource{
FieldRef: &corev1.ObjectFieldSelector{
FieldPath: "metadata.namespace",
},
},
},
{
Name: "SCAN_NAME",
Value: scan.Name,
},
}
 
// Starting a new job based on the current ReadAndWrite Hook
labels := scan.ObjectMeta.DeepCopy().Labels
if labels == nil {
labels = make(map[string]string)
}
if hook.Spec.Type == executionv1.ReadAndWrite {
labels["securecodebox.io/job-type"] = "read-and-write-hook"
} else if hook.Spec.Type == executionv1.ReadOnly {
labels["securecodebox.io/job-type"] = "read-only-hook"
}
labels["securecodebox.io/hook-name"] = hook.Name
 
var backOffLimit int32 = 3
truePointer := true
falsePointer := false
job := &batch.Job{
ObjectMeta: metav1.ObjectMeta{
Annotations: make(map[string]string),
GenerateName: util.TruncateName(fmt.Sprintf("%s-%s", hook.Name, scan.Name)),
Namespace: scan.Namespace,
Labels: labels,
},
Spec: batch.JobSpec{
BackoffLimit: &backOffLimit,
Template: corev1.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
"auto-discovery.securecodebox.io/ignore": "true",
"sidecar.istio.io/inject": "false",
},
},
Spec: corev1.PodSpec{
ServiceAccountName: serviceAccountName,
RestartPolicy: corev1.RestartPolicyNever,
ImagePullSecrets: hook.Spec.ImagePullSecrets,
Containers: []corev1.Container{
{
Name: "hook",
Image: hook.Spec.Image,
Args: cliArgs,
Env: append(hook.Spec.Env, standardEnvVars...),
ImagePullPolicy: "Always",
Resources: corev1.ResourceRequirements{
Requests: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("200m"),
corev1.ResourceMemory: resource.MustParse("100Mi"),
},
Limits: corev1.ResourceList{
corev1.ResourceCPU: resource.MustParse("400m"),
corev1.ResourceMemory: resource.MustParse("200Mi"),
},
},
SecurityContext: &corev1.SecurityContext{
RunAsNonRoot: &truePointer,
AllowPrivilegeEscalation: &falsePointer,
ReadOnlyRootFilesystem: &truePointer,
Privileged: &falsePointer,
Capabilities: &corev1.Capabilities{
Drop: []corev1.Capability{"all"},
},
},
},
},
},
},
TTLSecondsAfterFinished: nil,
},
}
if err := ctrl.SetControllerReference(scan, job, r.Scheme); err != nil {
r.Log.Error(err, "Unable to set controllerReference on job", "job", job)
return "", err
}
 
if err := r.Create(ctx, job); err != nil {
return "", err
}
return job.Name, nil
}
 
func (r *ScanReconciler) updateHookStatus(scan *executionv1.Scan, hookStatus executionv1.HookStatus) error {
for i, hook := range scan.Status.ReadAndWriteHookStatus {
if hook.HookName == hookStatus.HookName {
scan.Status.ReadAndWriteHookStatus[i] = hookStatus
break
}
}
if err := r.Status().Update(context.Background(), scan); err != nil {
r.Log.Error(err, "unable to update Scan status")
return err
}
return nil
}