Skip to content

Commit 768639a

Browse files
committed
✨ Add CronJob name generation functions and tests for container and k8s scans
✨ Implement cleanup of stale CronJobs in container and k8s scan handlers ✨ Add Workload Identity Federation support for container registry authentication and refactor related constants ✨ Update user manual with Workload Identity Federation setup instructions for GKE, EKS, and AKS ✨ Update wifInitContainer to generate kubeconfig with bearer token for GKE clusters 🧹 Refactor audit config tests to use dynamic cron job names for Kubernetes and container scans ✨ Enhance cron job name generation to handle long integration and cluster names 🧹 Fix cron job name validation to check for cluster name inclusion when not truncated
1 parent 423edbc commit 768639a

12 files changed

Lines changed: 701 additions & 233 deletions

File tree

.github/actions/spelling/expect.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
AAD
22
AADSTS
3-
acr
3+
Acr
44
artifactory
55
artifactregistry
6+
auditconfig
67
bak
78
bitnami
89
containerregistry
@@ -28,7 +29,6 @@ psat
2829
rolearn
2930
selfsigned
3031
servicemonitor
31-
servicemonitors
3232
spiffe
3333
SResources
3434
SVIDs

controllers/container_image/deployment_handler.go

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,8 @@ type DeploymentHandler struct {
2929
}
3030

3131
func (n *DeploymentHandler) Reconcile(ctx context.Context) (ctrl.Result, error) {
32-
// TODO: remove in next version
33-
// Delete the old container scanning cronjob if it exists
34-
if err := k8s.DeleteIfExists(ctx,
35-
n.KubeClient,
36-
&batchv1.CronJob{ObjectMeta: metav1.ObjectMeta{
37-
Name: OldCronJobName(n.Mondoo.Name),
38-
Namespace: n.Mondoo.Namespace,
39-
}}); err != nil {
32+
// Clean up CronJobs with stale names (from old naming schemes)
33+
if err := n.cleanupStaleCronJobs(ctx); err != nil {
4034
return ctrl.Result{}, err
4135
}
4236

@@ -235,3 +229,26 @@ func (n *DeploymentHandler) cleanupWIFServiceAccount(ctx context.Context) error
235229
}
236230
return nil
237231
}
232+
233+
// cleanupStaleCronJobs removes CronJobs from old naming schemes by label selection.
234+
func (n *DeploymentHandler) cleanupStaleCronJobs(ctx context.Context) error {
235+
cronJobs := &batchv1.CronJobList{}
236+
listOpts := &client.ListOptions{
237+
Namespace: n.Mondoo.Namespace,
238+
LabelSelector: labels.SelectorFromSet(CronJobLabels(*n.Mondoo)),
239+
}
240+
if err := n.KubeClient.List(ctx, cronJobs, listOpts); err != nil {
241+
return err
242+
}
243+
244+
expectedName := CronJobName(n.Mondoo.Name)
245+
for i := range cronJobs.Items {
246+
if cronJobs.Items[i].Name != expectedName {
247+
logger.Info("Deleting stale container scan CronJob", "name", cronJobs.Items[i].Name)
248+
if err := k8s.DeleteIfExists(ctx, n.KubeClient, &cronJobs.Items[i]); err != nil {
249+
return err
250+
}
251+
}
252+
}
253+
return nil
254+
}

controllers/container_image/resources.go

Lines changed: 6 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,13 @@ import (
1111
// That's the mod k8s relies on https://github.com/kubernetes/kubernetes/blob/master/go.mod#L63
1212

1313
"go.mondoo.com/mondoo-operator/api/v1alpha2"
14-
k8s_scan "go.mondoo.com/mondoo-operator/controllers/k8s_scan"
1514
"go.mondoo.com/mondoo-operator/pkg/constants"
1615
"go.mondoo.com/mondoo-operator/pkg/feature_flags"
1716
"go.mondoo.com/mondoo-operator/pkg/utils/k8s"
1817
mondoo "go.mondoo.com/mondoo-operator/pkg/utils/mondoo"
1918
"go.mondoo.com/mql/v13/providers-sdk/v1/inventory"
2019
batchv1 "k8s.io/api/batch/v1"
2120
corev1 "k8s.io/api/core/v1"
22-
"k8s.io/apimachinery/pkg/api/resource"
2321
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2422
"k8s.io/utils/ptr"
2523
"sigs.k8s.io/yaml"
@@ -193,7 +191,7 @@ func CronJob(image, integrationMrn, clusterUid, privateRegistrySecretName string
193191
)
194192

195193
// Add init container for registry credential generation
196-
podSpec.InitContainers = append(podSpec.InitContainers, registryWIFInitContainer(wif))
194+
podSpec.InitContainers = append(podSpec.InitContainers, k8s.RegistryWIFInitContainer(wif))
197195

198196
// AKS Workload Identity webhook requires this label on the pod template only.
199197
// Copy labels so we don't mutate the CronJob/Job metadata.
@@ -232,6 +230,11 @@ func OldCronJobName(prefix string) string {
232230
}
233231

234232
func CronJobName(prefix string) string {
233+
return k8s.CronJobName("container-scan", prefix)
234+
}
235+
236+
// LegacyCronJobName returns the pre-v14 name format: {prefix}-containers-scan
237+
func LegacyCronJobName(prefix string) string {
235238
return fmt.Sprintf("%s%s", prefix, CronJobNameSuffix)
236239
}
237240

@@ -380,175 +383,3 @@ func validateContainerRegistryWIF(wif *v1alpha2.WorkloadIdentityConfig) error {
380383

381384
return nil
382385
}
383-
384-
// registryWIFInitContainer creates an init container that generates docker config credentials
385-
// using cloud-native Workload Identity Federation
386-
func registryWIFInitContainer(wif *v1alpha2.WorkloadIdentityConfig) corev1.Container {
387-
var image, shell, script string
388-
var env []corev1.EnvVar
389-
390-
// Common retry wrapper for transient failures
391-
retryWrapper := `set -euo pipefail
392-
# Retry wrapper for transient failures
393-
retry() {
394-
local max_attempts=3
395-
local delay=5
396-
local attempt=1
397-
while [ $attempt -le $max_attempts ]; do
398-
if "$@"; then
399-
return 0
400-
fi
401-
echo "Attempt $attempt failed, retrying in ${delay}s..."
402-
sleep $delay
403-
attempt=$((attempt + 1))
404-
done
405-
echo "All $max_attempts attempts failed"
406-
return 1
407-
}
408-
`
409-
410-
switch wif.Provider {
411-
case v1alpha2.CloudProviderGKE:
412-
image = k8s_scan.GCloudSDKImage
413-
shell = "/bin/bash"
414-
script = retryWrapper + `
415-
# Use WIF identity to get an access token for Artifact Registry / GCR
416-
TOKEN=$(retry gcloud auth print-access-token)
417-
AUTH=$(echo -n "oauth2accesstoken:${TOKEN}" | base64 -w0)
418-
419-
# All GCP regions and multi-region locations that host Artifact Registry.
420-
# Docker config requires exact hostname matches, so we enumerate them all.
421-
AR_LOCATIONS="
422-
africa-south1 asia-east1 asia-east2 asia-northeast1 asia-northeast2 asia-northeast3
423-
asia-south1 asia-south2 asia-southeast1 asia-southeast2
424-
australia-southeast1 australia-southeast2
425-
europe-central2 europe-north1 europe-southwest1 europe-west1 europe-west2
426-
europe-west3 europe-west4 europe-west6 europe-west8 europe-west9 europe-west10 europe-west12
427-
me-central1 me-central2 me-west1
428-
northamerica-northeast1 northamerica-northeast2
429-
southamerica-east1 southamerica-west1
430-
us-central1 us-east1 us-east4 us-east5 us-south1 us-west1 us-west2 us-west3 us-west4
431-
asia europe us
432-
"
433-
434-
AUTHS=""
435-
add_auth() {
436-
[ -n "$AUTHS" ] && AUTHS="${AUTHS},"
437-
AUTHS="${AUTHS}\"$1\":{\"auth\":\"${AUTH}\"}"
438-
}
439-
440-
for loc in $AR_LOCATIONS; do
441-
add_auth "${loc}-docker.pkg.dev"
442-
done
443-
444-
# Legacy GCR endpoints
445-
for host in gcr.io us.gcr.io eu.gcr.io asia.gcr.io; do
446-
add_auth "$host"
447-
done
448-
449-
cat > /etc/opt/mondoo/docker/config.json <<DOCKEREOF
450-
{"auths":{${AUTHS}}}
451-
DOCKEREOF
452-
echo "Docker config generated for $(echo "$AUTHS" | tr ',' '\n' | wc -l) registry endpoints"
453-
`
454-
env = []corev1.EnvVar{
455-
{Name: "HOME", Value: "/tmp"},
456-
}
457-
458-
case v1alpha2.CloudProviderEKS:
459-
image = k8s_scan.AWSCLIImage
460-
shell = "/bin/bash"
461-
script = retryWrapper + `
462-
# Use IRSA identity to get ECR login password
463-
PASSWORD=$(retry aws ecr get-login-password --region "$AWS_REGION")
464-
465-
# Derive registry URL from role ARN account ID and region
466-
ACCOUNT_ID=$(echo "$ROLE_ARN" | cut -d: -f5)
467-
REGISTRY="${ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com"
468-
469-
# Write docker config
470-
AUTH=$(echo -n "AWS:${PASSWORD}" | base64 -w0)
471-
cat > /etc/opt/mondoo/docker/config.json <<DOCKEREOF
472-
{
473-
"auths": {
474-
"${REGISTRY}": { "auth": "${AUTH}" }
475-
}
476-
}
477-
DOCKEREOF
478-
echo "Docker config generated for ECR registry: ${REGISTRY}"
479-
`
480-
env = []corev1.EnvVar{
481-
{Name: "HOME", Value: "/tmp"},
482-
{Name: "AWS_REGION", Value: wif.EKS.Region},
483-
{Name: "ROLE_ARN", Value: wif.EKS.RoleARN},
484-
}
485-
486-
case v1alpha2.CloudProviderAKS:
487-
image = k8s_scan.AzureCLIImage
488-
shell = "/bin/bash"
489-
script = retryWrapper + `
490-
# Azure WIF webhook injects AZURE_CLIENT_ID, AZURE_TENANT_ID, AZURE_FEDERATED_TOKEN_FILE
491-
retry az login --federated-token "$(cat "$AZURE_FEDERATED_TOKEN_FILE")" \
492-
--service-principal \
493-
-u "$AZURE_CLIENT_ID" \
494-
-t "$AZURE_TENANT_ID"
495-
496-
# Get ACR access token
497-
TOKEN=$(retry az acr login --name "$ACR_LOGIN_SERVER" --expose-token --output tsv --query accessToken)
498-
499-
# Write docker config
500-
AUTH=$(echo -n "00000000-0000-0000-0000-000000000000:${TOKEN}" | base64 -w0)
501-
cat > /etc/opt/mondoo/docker/config.json <<DOCKEREOF
502-
{
503-
"auths": {
504-
"${ACR_LOGIN_SERVER}": { "auth": "${AUTH}" }
505-
}
506-
}
507-
DOCKEREOF
508-
echo "Docker config generated for ACR: ${ACR_LOGIN_SERVER}"
509-
`
510-
env = []corev1.EnvVar{
511-
{Name: "HOME", Value: "/tmp"},
512-
{Name: "ACR_LOGIN_SERVER", Value: wif.AKS.LoginServer},
513-
}
514-
515-
default:
516-
image = "busybox:1.36"
517-
shell = "/bin/sh"
518-
script = `echo "ERROR: Unknown workload identity provider"; exit 1`
519-
env = []corev1.EnvVar{}
520-
}
521-
522-
return corev1.Container{
523-
Name: "generate-registry-creds",
524-
Image: image,
525-
ImagePullPolicy: corev1.PullIfNotPresent,
526-
Command: []string{shell, "-c", script},
527-
Env: env,
528-
VolumeMounts: []corev1.VolumeMount{
529-
{Name: "docker-config", MountPath: "/etc/opt/mondoo/docker"},
530-
{Name: "temp", MountPath: "/tmp"},
531-
},
532-
Resources: corev1.ResourceRequirements{
533-
Requests: corev1.ResourceList{
534-
corev1.ResourceCPU: resource.MustParse("50m"),
535-
corev1.ResourceMemory: resource.MustParse("64Mi"),
536-
},
537-
Limits: corev1.ResourceList{
538-
corev1.ResourceCPU: resource.MustParse("200m"),
539-
corev1.ResourceMemory: resource.MustParse("256Mi"),
540-
},
541-
},
542-
TerminationMessagePath: "/dev/termination-log",
543-
TerminationMessagePolicy: corev1.TerminationMessageReadFile,
544-
SecurityContext: &corev1.SecurityContext{
545-
AllowPrivilegeEscalation: ptr.To(false),
546-
ReadOnlyRootFilesystem: ptr.To(true),
547-
RunAsNonRoot: ptr.To(true),
548-
RunAsUser: ptr.To(int64(101)),
549-
Capabilities: &corev1.Capabilities{
550-
Drop: []corev1.Capability{"ALL"},
551-
},
552-
},
553-
}
554-
}

controllers/k8s_scan/deployment_handler.go

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,11 @@ type DeploymentHandler struct {
141141
}
142142

143143
func (n *DeploymentHandler) Reconcile(ctx context.Context) (ctrl.Result, error) {
144+
// Clean up CronJobs with stale names (from old naming schemes)
145+
if err := n.cleanupStaleCronJobs(ctx); err != nil {
146+
return ctrl.Result{}, err
147+
}
148+
144149
hasExternalClusters := len(n.Mondoo.Spec.KubernetesResources.ExternalClusters) > 0
145150

146151
if !n.Mondoo.Spec.KubernetesResources.Enable {
@@ -718,3 +723,43 @@ func (n *DeploymentHandler) syncWIFServiceAccount(ctx context.Context, cluster v
718723

719724
return nil
720725
}
726+
727+
// cleanupStaleCronJobs removes CronJobs from old naming schemes by label selection.
728+
// CronJobs belonging to removed external clusters are skipped here — cleanupOrphanedExternalClusterResources
729+
// handles those so it can also clean up associated ConfigMaps, ServiceAccounts, and Secrets.
730+
func (n *DeploymentHandler) cleanupStaleCronJobs(ctx context.Context) error {
731+
cronJobs := &batchv1.CronJobList{}
732+
listOpts := &client.ListOptions{
733+
Namespace: n.Mondoo.Namespace,
734+
LabelSelector: labels.SelectorFromSet(map[string]string{
735+
"app": "mondoo-k8s-scan",
736+
"mondoo_cr": n.Mondoo.Name,
737+
}),
738+
}
739+
if err := n.KubeClient.List(ctx, cronJobs, listOpts); err != nil {
740+
return err
741+
}
742+
743+
expected := map[string]bool{
744+
CronJobName(n.Mondoo.Name): true,
745+
}
746+
configuredClusters := make(map[string]bool)
747+
for _, cluster := range n.Mondoo.Spec.KubernetesResources.ExternalClusters {
748+
expected[ExternalClusterCronJobName(n.Mondoo.Name, cluster.Name)] = true
749+
configuredClusters[cluster.Name] = true
750+
}
751+
752+
for i := range cronJobs.Items {
753+
if expected[cronJobs.Items[i].Name] {
754+
continue
755+
}
756+
if clusterName, ok := cronJobs.Items[i].Labels["cluster_name"]; ok && !configuredClusters[clusterName] {
757+
continue
758+
}
759+
logger.Info("Deleting stale k8s scan CronJob", "name", cronJobs.Items[i].Name)
760+
if err := k8s.DeleteIfExists(ctx, n.KubeClient, &cronJobs.Items[i]); err != nil {
761+
return err
762+
}
763+
}
764+
return nil
765+
}

controllers/k8s_scan/deployment_handler_test.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1530,11 +1530,11 @@ func TestExternalClusterNaming(t *testing.T) {
15301530
t.Run(tt.prefix+"-"+tt.clusterName, func(t *testing.T) {
15311531
// Test CronJob name
15321532
cronJobName := ExternalClusterCronJobName(tt.prefix, tt.clusterName)
1533-
if !strings.HasPrefix(cronJobName, tt.prefix) {
1534-
t.Errorf("CronJob name should start with prefix %q, got %q", tt.prefix, cronJobName)
1533+
if !strings.HasPrefix(cronJobName, "mondoo-k8s-scan-") {
1534+
t.Errorf("CronJob name should start with %q, got %q", "mondoo-k8s-scan-", cronJobName)
15351535
}
1536-
if !strings.HasSuffix(cronJobName, tt.clusterName) {
1537-
t.Errorf("CronJob name should end with cluster name %q, got %q", tt.clusterName, cronJobName)
1536+
if !strings.Contains(cronJobName, tt.clusterName) && len(cronJobName) < 52 {
1537+
t.Errorf("CronJob name should contain cluster name %q when not truncated, got %q", tt.clusterName, cronJobName)
15381538
}
15391539

15401540
// Test ConfigMap name

0 commit comments

Comments
 (0)