diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index c8dc05f11..76027123b 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -1,20 +1,24 @@ AADSTS +artifactory bak bitnami curlimages deepcopy deletecollection +dockerconfigjson eksctl fullname iamidentitymapping irsa kustomization mcr +mondoooperatorconfig oidc openssl psat rolearn selfsigned +servicemonitor servicemonitors spiffe SVIDs diff --git a/api/v1alpha2/mondoooperatorconfig_types.go b/api/v1alpha2/mondoooperatorconfig_types.go index 5909737b3..10df1583b 100644 --- a/api/v1alpha2/mondoooperatorconfig_types.go +++ b/api/v1alpha2/mondoooperatorconfig_types.go @@ -14,22 +14,43 @@ const ( MondooOperatorConfigName = "mondoo-operator-config" ) -// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! -// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. - // MondooOperatorConfigSpec defines the desired state of MondooOperatorConfig type MondooOperatorConfigSpec struct { - // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster - // Important: Run "make" to regenerate code after modifying this file - // Metrics controls the enabling/disabling of metrics report of mondoo-operator Metrics Metrics `json:"metrics,omitempty"` // Allows skipping Image resolution from upstream repository SkipContainerResolution bool `json:"skipContainerResolution,omitempty"` // HttpProxy specifies a proxy to use for HTTP requests to the Mondoo Platform. HttpProxy *string `json:"httpProxy,omitempty"` + // HttpsProxy specifies a proxy to use for HTTPS requests to the Mondoo Platform. + HttpsProxy *string `json:"httpsProxy,omitempty"` + // NoProxy specifies a comma-separated list of hosts that should not use the proxy. + NoProxy *string `json:"noProxy,omitempty"` // ContainerProxy specifies a proxy to use for container images. ContainerProxy *string `json:"containerProxy,omitempty"` + // ImagePullSecrets specifies the name of the Secret to use for pulling images for all Mondoo components. + // The secret must be of type kubernetes.io/dockerconfigjson. + ImagePullSecrets []corev1.LocalObjectReference `json:"imagePullSecrets,omitempty"` + // ImageRegistry specifies a custom container image registry prefix for all Mondoo images. + // Use this for simple registry mirrors where all images go to the same mirror. + // Example: "artifactory.example.com/ghcr.io.docker" + // For more complex setups with multiple source registries, use RegistryMirrors instead. + ImageRegistry *string `json:"imageRegistry,omitempty"` + // RegistryMirrors specifies a mapping of public registries to private mirrors. + // Use this when you need to map different source registries to different mirrors. + // The key is the public registry (e.g., "ghcr.io", "docker.io", "quay.io") + // and the value is the private mirror (e.g., "artifactory.example.com/ghcr.io.docker"). + // Example: + // registryMirrors: + // ghcr.io: artifactory.example.com/ghcr.io.docker + // docker.io: artifactory.example.com/hub.docker.com + // Note: If both ImageRegistry and RegistryMirrors are set, RegistryMirrors takes precedence. + RegistryMirrors map[string]string `json:"registryMirrors,omitempty"` + // SkipProxyForCnspec disables proxy environment variables for cnspec-based components + // (scan-api, container scanning). Use this when the Mondoo API is accessible directly + // without proxy (e.g., internal mirror) but other components need proxy for external access. + // Default: false (proxy settings are applied to all components) + SkipProxyForCnspec bool `json:"skipProxyForCnspec,omitempty"` } type Metrics struct { @@ -41,9 +62,6 @@ type Metrics struct { // MondooOperatorConfigStatus defines the observed state of MondooOperatorConfig type MondooOperatorConfigStatus struct { - // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster - // Important: Run "make" to regenerate code after modifying this file - // Conditions includes more detailed status for the mondoo config // +optional Conditions []MondooOperatorConfigCondition `json:"conditions,omitempty"` diff --git a/api/v1alpha2/zz_generated.deepcopy.go b/api/v1alpha2/zz_generated.deepcopy.go index f94488396..32cc0b08a 100644 --- a/api/v1alpha2/zz_generated.deepcopy.go +++ b/api/v1alpha2/zz_generated.deepcopy.go @@ -514,11 +514,38 @@ func (in *MondooOperatorConfigSpec) DeepCopyInto(out *MondooOperatorConfigSpec) *out = new(string) **out = **in } + if in.HttpsProxy != nil { + in, out := &in.HttpsProxy, &out.HttpsProxy + *out = new(string) + **out = **in + } + if in.NoProxy != nil { + in, out := &in.NoProxy, &out.NoProxy + *out = new(string) + **out = **in + } if in.ContainerProxy != nil { in, out := &in.ContainerProxy, &out.ContainerProxy *out = new(string) **out = **in } + if in.ImagePullSecrets != nil { + in, out := &in.ImagePullSecrets, &out.ImagePullSecrets + *out = make([]v1.LocalObjectReference, len(*in)) + copy(*out, *in) + } + if in.ImageRegistry != nil { + in, out := &in.ImageRegistry, &out.ImageRegistry + *out = new(string) + **out = **in + } + if in.RegistryMirrors != nil { + in, out := &in.RegistryMirrors, &out.RegistryMirrors + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MondooOperatorConfigSpec. diff --git a/charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml b/charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml new file mode 100644 index 000000000..bc05d9776 --- /dev/null +++ b/charts/mondoo-operator/crds/k8s.mondoo.com_mondooauditconfigs.yaml @@ -0,0 +1,1302 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: mondooauditconfigs.k8s.mondoo.com +spec: + group: k8s.mondoo.com + names: + kind: MondooAuditConfig + listKind: MondooAuditConfigList + plural: mondooauditconfigs + singular: mondooauditconfig + scope: Namespaced + versions: + - name: v1alpha2 + schema: + openAPIV3Schema: + description: MondooAuditConfig is the Schema for the mondooauditconfigs API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: MondooAuditConfigSpec defines the desired state of MondooAuditConfig + properties: + admission: + description: |- + Admission is DEPRECATED and ignored. Admission webhooks were removed in v12.1.0. + The operator will automatically clean up any orphaned admission resources. + See docs/admission-migration-guide.md for migration instructions. + properties: + certificateProvisioning: + description: CertificateProvisioning is DEPRECATED. + properties: + mode: + description: Mode is DEPRECATED. + type: string + type: object + enable: + description: Enable is DEPRECATED. Admission webhooks are no longer + supported. + type: boolean + image: + description: Image is DEPRECATED. + properties: + digest: + description: |- + Digest specifies the image digest (e.g., sha256:abc123...). + When specified, this takes precedence over Tag. + type: string + name: + type: string + tag: + type: string + type: object + mode: + description: Mode is DEPRECATED. + type: string + replicas: + description: Replicas is DEPRECATED. + format: int32 + type: integer + serviceAccountName: + description: ServiceAccountName is DEPRECATED. + type: string + type: object + annotations: + additionalProperties: + type: string + description: |- + Annotations allows adding custom annotations to all scanned assets. These key-value pairs + will be attached to every asset discovered by the operator, making them searchable + and filterable in the Mondoo Console. + type: object + consoleIntegration: + properties: + enable: + type: boolean + type: object + containers: + properties: + enable: + type: boolean + env: + description: |- + Env allows setting extra environment variables for the node scanner. If the operator sets already an env + variable with the same name, the value specified here will override it. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + resources: + description: ResourceRequirements describes the compute resource + requirements. + 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 + schedule: + description: Specify a custom crontab schedule for the container + image scanning job. If not specified, the default schedule is + used. + type: string + type: object + filtering: + properties: + namespaces: + properties: + exclude: + description: |- + Exclude is the list of resources to ignore for any watching/scanning actions. Use this if + the goal is to watch/scan all resources except for this Exclude list. + items: + type: string + type: array + include: + description: |- + Include is the list of resources to watch/scan. Setting Include overrides anything in the + Exclude list as specifying an Include list is effectively excluding everything except for what + is on the Include list. + items: + type: string + type: array + type: object + type: object + kubernetesResources: + properties: + containerImageScanning: + description: |- + DEPRECATED: ContainerImageScanning determines whether container images are being scanned. The current implementation + runs a separate job once every 24h that scans the container images running in the cluster. + type: boolean + enable: + type: boolean + externalClusters: + description: |- + ExternalClusters defines remote K8s clusters to scan from this operator instance. + Each external cluster will have its own CronJob created with the appropriate kubeconfig. + items: + description: ExternalCluster defines configuration for scanning + a remote K8s cluster + properties: + containerImageScanning: + description: ContainerImageScanning enables scanning of + container images in this external cluster. + type: boolean + filtering: + description: |- + Filtering allows namespace filtering specific to this external cluster. + If not specified, uses the global filtering from MondooAuditConfigSpec.Filtering. + properties: + namespaces: + properties: + exclude: + description: |- + Exclude is the list of resources to ignore for any watching/scanning actions. Use this if + the goal is to watch/scan all resources except for this Exclude list. + items: + type: string + type: array + include: + description: |- + Include is the list of resources to watch/scan. Setting Include overrides anything in the + Exclude list as specifying an Include list is effectively excluding everything except for what + is on the Include list. + items: + type: string + type: array + type: object + type: object + kubeconfigSecretRef: + description: |- + KubeconfigSecretRef references a Secret containing kubeconfig for the remote cluster. + The Secret must have a key "kubeconfig" with the kubeconfig content. + Mutually exclusive with ServiceAccountAuth and WorkloadIdentity. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + name: + description: |- + Name is a unique identifier for this cluster (used in resource names). + Must be a valid Kubernetes name (lowercase, alphanumeric, hyphens allowed). + pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ + type: string + privateRegistriesPullSecretRef: + description: |- + PrivateRegistriesPullSecretRef references a Secret containing registry credentials + for pulling/scanning private images in this remote cluster. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + schedule: + description: |- + Schedule overrides the default schedule for this cluster (optional). + If not specified, uses the schedule from KubernetesResources.Schedule. + type: string + serviceAccountAuth: + description: |- + ServiceAccountAuth configures authentication using a service account token. + Mutually exclusive with KubeconfigSecretRef and WorkloadIdentity. + properties: + credentialsSecretRef: + description: |- + CredentialsSecretRef references a Secret containing: + - "token": The service account token (required) + - "ca.crt": The CA certificate (required) + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + server: + description: |- + Server is the URL of the Kubernetes API server. + Example: "https://my-cluster.example.com:6443" + pattern: ^https://.* + type: string + skipTLSVerify: + default: false + description: SkipTLSVerify skips TLS verification. NOT + RECOMMENDED for production. + type: boolean + required: + - credentialsSecretRef + - server + type: object + spiffeAuth: + description: |- + SPIFFEAuth configures SPIFFE/SPIRE-based authentication using X.509 SVIDs. + Mutually exclusive with KubeconfigSecretRef, ServiceAccountAuth, and WorkloadIdentity. + properties: + audience: + description: |- + Audience is the intended audience for the SVID (optional). + Some SPIRE configurations require this for workload attestation. + type: string + server: + description: |- + Server is the URL of the remote Kubernetes API server. + Example: "https://remote-cluster.example.com:6443" + pattern: ^https://.* + type: string + socketPath: + default: /run/spire/sockets/agent.sock + description: |- + SocketPath is the path to the SPIRE agent's Workload API socket. + Defaults to "/run/spire/sockets/agent.sock" if not specified. + type: string + trustBundleSecretRef: + description: |- + TrustBundleSecretRef references a Secret containing the remote cluster's + CA certificate for TLS verification. The Secret must have a key "ca.crt". + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + required: + - server + - trustBundleSecretRef + type: object + workloadIdentity: + description: |- + WorkloadIdentity configures cloud-native Workload Identity Federation authentication. + Mutually exclusive with KubeconfigSecretRef, ServiceAccountAuth, and SPIFFEAuth. + properties: + aks: + description: |- + AKS contains AKS-specific Azure Workload Identity configuration. + Required when provider is "aks". + properties: + clientId: + description: ClientID is the Azure AD app client + ID. + type: string + clusterName: + description: ClusterName is the AKS cluster name. + type: string + resourceGroup: + description: ResourceGroup containing the AKS cluster. + type: string + subscriptionId: + description: SubscriptionID is the Azure subscription. + type: string + tenantId: + description: TenantID is the Azure AD tenant ID. + type: string + required: + - clientId + - clusterName + - resourceGroup + - subscriptionId + - tenantId + type: object + eks: + description: |- + EKS contains EKS-specific IRSA configuration. + Required when provider is "eks". + properties: + clusterName: + description: ClusterName is the EKS cluster name. + type: string + region: + description: Region is the AWS region. + type: string + roleArn: + description: |- + RoleARN is the IAM role to assume. + Format: arn:aws:iam:::role/ + type: string + required: + - clusterName + - region + - roleArn + type: object + gke: + description: |- + GKE contains GKE-specific Workload Identity configuration. + Required when provider is "gke". + properties: + clusterLocation: + description: ClusterLocation is the region or zone + (e.g., "us-central1" or "us-central1-a"). + type: string + clusterName: + description: ClusterName is the GKE cluster name. + type: string + googleServiceAccount: + description: |- + GoogleServiceAccount is the Google service account to impersonate. + Format: @.iam.gserviceaccount.com + type: string + projectId: + description: ProjectID is the GCP project ID. + type: string + required: + - clusterLocation + - clusterName + - googleServiceAccount + - projectId + type: object + provider: + description: Provider specifies the cloud provider for + WIF. + enum: + - gke + - eks + - aks + type: string + required: + - provider + type: object + required: + - name + type: object + type: array + resourceWatcher: + description: |- + ResourceWatcher configures real-time resource watching and scanning. + When enabled, a deployment will be created that watches for K8s resource changes + and scans them immediately rather than waiting for the CronJob schedule. + properties: + debounceInterval: + default: 10s + description: |- + DebounceInterval specifies how long to batch changes before triggering a scan. + This prevents excessive scanning when multiple resources change in quick succession. + Default is 10 seconds. + type: string + enable: + description: |- + Enable enables real-time resource watching and scanning. + When enabled, a deployment will be created that watches K8s resources for changes + and scans them using cnspec. + type: boolean + minimumScanInterval: + default: 2m + description: |- + MinimumScanInterval specifies the minimum time between scans (rate limit). + This provides a hard limit on scan frequency even when resources are changing continuously. + Default is 2 minutes. + type: string + resourceTypes: + description: |- + ResourceTypes specifies which resource types to watch. If not specified, defaults are used + based on WatchAllResources setting. When WatchAllResources is false (default), defaults to: + deployments, daemonsets, statefulsets, replicasets. When true, defaults to: + pods, deployments, daemonsets, statefulsets, replicasets, jobs, cronjobs, services, ingresses, namespaces + items: + type: string + type: array + watchAllResources: + description: |- + WatchAllResources controls whether to watch all resource types or only high-priority ones. + When false (default), only watches stable workload resources: Deployments, DaemonSets, + StatefulSets, and ReplicaSets. When true, watches all resources including ephemeral ones + like Pods, Jobs, and CronJobs. + type: boolean + type: object + schedule: + description: Specify a custom crontab schedule for the Kubernetes + resource scanning job. If not specified, the default schedule + is used. + type: string + type: object + mondooCredsSecretRef: + description: Config is an example field of MondooAuditConfig. Edit + mondooauditconfig_types.go to remove/update + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + mondooTokenSecretRef: + description: |- + MondooTokenSecretRef can optionally hold a time-limited token that the mondoo-operator will use + to create a Mondoo service account saved to the Secret specified in .spec.mondooCredsSecretRef + if that Secret does not exist. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + nodes: + properties: + enable: + type: boolean + env: + description: |- + Env allows setting extra environment variables for the node scanner. If the operator sets already an env + variable with the same name, the value specified here will override it. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + intervalTimer: + default: 60 + description: |- + IntervalTimer is the interval (in minutes) for the node scanning. The default is "60". Only applicable for Deployment + style. + type: integer + priorityClassName: + description: PriorityClassName specifies the name of the PriorityClass + for the node scanning workloads. + type: string + resources: + description: ResourceRequirements describes the compute resource + requirements. + 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 + schedule: + description: |- + Schedule specifies a custom crontab schedule for the node scanning job. If not specified, the default schedule is + used. Only applicable for CronJob style + type: string + style: + default: cronjob + description: Style specifies how node scanning is deployed. The + default is "cronjob" which will create a CronJob for the node + scanning. + enum: + - cronjob + - deployment + - daemonset + type: string + type: object + scanner: + description: |- + Scanner defines the settings for the Mondoo scanner that will be running in the cluster. The same scanner + is used for scanning the Kubernetes API and the nodes. + properties: + env: + description: |- + Env allows setting extra environment variables for the scanner. If the operator sets already an env + variable with the same name, the value specified here will override it. + items: + description: EnvVar represents an environment variable present + in a Container. + properties: + name: + description: |- + Name of the environment variable. + May consist of any printable ASCII characters except '='. + type: string + value: + description: |- + Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the container and + any service environment variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless of whether the variable + exists or not. + Defaults to "". + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + fieldRef: + description: |- + Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. + properties: + apiVersion: + description: Version of the schema the FieldPath + is written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the + specified API version. + type: string + required: + - fieldPath + type: object + x-kubernetes-map-type: atomic + fileKeyRef: + description: |- + FileKeyRef selects a key of the env file. + Requires the EnvFiles feature gate to be enabled. + properties: + key: + description: |- + The key within the env file. An invalid key will prevent the pod from starting. + The keys defined within a source may consist of any printable ASCII characters except '='. + During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. + type: string + optional: + default: false + description: |- + Specify whether the file or its key must be defined. If the file or key + does not exist, then the env var is not published. + If optional is set to true and the specified key does not exist, + the environment variable will not be set in the Pod's containers. + + If optional is set to false and the specified key does not exist, + an error will be returned during Pod creation. + type: boolean + path: + description: |- + The path within the volume from which to select the file. + Must be relative and may not contain the '..' path or start with '..'. + type: string + volumeName: + description: The name of the volume mount containing + the env file. + type: string + required: + - key + - path + - volumeName + type: object + x-kubernetes-map-type: atomic + resourceFieldRef: + description: |- + Selects a resource of the container: only resources limits and requests + (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the + exposed resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + x-kubernetes-map-type: atomic + secretKeyRef: + description: Selects a key of a secret in the pod's + namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + x-kubernetes-map-type: atomic + type: object + required: + - name + type: object + type: array + image: + properties: + digest: + description: |- + Digest specifies the image digest (e.g., sha256:abc123...). + When specified, this takes precedence over Tag. + type: string + name: + type: string + tag: + type: string + type: object + privateRegistriesPullSecretRef: + description: |- + PrivateRegistriesPullSecretRef defines the name of a secret that contains the credentials for the private + registries we have to pull images from. Use this when you have a single secret containing credentials + for one or more registries. For multiple separate secrets, use PrivateRegistriesPullSecretRefs instead. + Deprecated: Use PrivateRegistriesPullSecretRefs for new configurations. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + privateRegistriesPullSecretRefs: + description: |- + PrivateRegistriesPullSecretRefs defines a list of secrets that contain credentials for private + registries. Use this when you need to pull images from multiple private registries and the credentials + are stored in separate secrets (e.g., managed by different teams or external secret operators). + The credentials from all secrets will be merged. If both PrivateRegistriesPullSecretRef and + PrivateRegistriesPullSecretRefs are specified, all secrets will be merged together. + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + replicas: + default: 1 + description: |- + Number of replicas for the scanner. + For enforcing mode, the minimum should be two to prevent problems during Pod failures, + e.g. node failure, node scaling, etc. + format: int32 + minimum: 1 + type: integer + resources: + description: ResourceRequirements describes the compute resource + requirements. + 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 + serviceAccountName: + default: mondoo-operator-k8s-resources-scanning + type: string + type: object + required: + - mondooCredsSecretRef + type: object + status: + description: MondooAuditConfigStatus defines the observed state of MondooAuditConfig + properties: + conditions: + description: Conditions includes detailed status for the MondooAuditConfig + items: + properties: + affectedPods: + description: AffectedPods, when filled, contains a list which + are affected by an issue + items: + type: string + type: array + lastTransitionTime: + description: LastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: LastUpdateTime is the last time we probed the condition + format: date-time + type: string + memoryLimit: + description: MemoryLimit contains the currently active memory + limit for a Pod + type: string + message: + description: Message is a human-readable message indicating + details about the last transition + type: string + reason: + description: Reason is a unique, one-word, CamelCase reason + for the condition's last transition + type: string + status: + description: Status is the status of the condition + type: string + type: + description: Type is the specific type of the condition + type: string + required: + - status + - type + type: object + type: array + lastK8sResourceGarbageCollectionTime: + description: |- + LastK8sResourceGarbageCollectionTime tracks the last time the operator performed + garbage collection of stale K8s resource scan assets. + format: date-time + type: string + pods: + description: Pods store the name of the pods which are running mondoo + instances + items: + type: string + type: array + reconciledByOperatorVersion: + description: ReconciledByOperatorVersion contains the version of the + operator which reconciled this MondooAuditConfig + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/mondoo-operator/crds/k8s.mondoo.com_mondoooperatorconfigs.yaml b/charts/mondoo-operator/crds/k8s.mondoo.com_mondoooperatorconfigs.yaml new file mode 100644 index 000000000..3121a0c37 --- /dev/null +++ b/charts/mondoo-operator/crds/k8s.mondoo.com_mondoooperatorconfigs.yaml @@ -0,0 +1,169 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.20.0 + name: mondoooperatorconfigs.k8s.mondoo.com +spec: + group: k8s.mondoo.com + names: + kind: MondooOperatorConfig + listKind: MondooOperatorConfigList + plural: mondoooperatorconfigs + singular: mondoooperatorconfig + scope: Cluster + versions: + - name: v1alpha2 + schema: + openAPIV3Schema: + description: MondooOperatorConfig is the Schema for the mondoooperatorconfigs + API + properties: + apiVersion: + description: |- + APIVersion defines the versioned schema of this representation of an object. + Servers should convert recognized schemas to the latest internal value, and + may reject unrecognized values. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources + type: string + kind: + description: |- + Kind is a string value representing the REST resource this object represents. + Servers may infer this from the endpoint the client submits requests to. + Cannot be updated. + In CamelCase. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds + type: string + metadata: + type: object + spec: + description: MondooOperatorConfigSpec defines the desired state of MondooOperatorConfig + properties: + containerProxy: + description: ContainerProxy specifies a proxy to use for container + images. + type: string + httpProxy: + description: HttpProxy specifies a proxy to use for HTTP requests + to the Mondoo Platform. + type: string + httpsProxy: + description: HttpsProxy specifies a proxy to use for HTTPS requests + to the Mondoo Platform. + type: string + imagePullSecrets: + description: |- + ImagePullSecrets specifies the name of the Secret to use for pulling images for all Mondoo components. + The secret must be of type kubernetes.io/dockerconfigjson. + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + imageRegistry: + description: |- + ImageRegistry specifies a custom container image registry to use for all Mondoo images. + This allows using a private registry mirror (e.g., artifactory.example.com/ghcr.io.docker). + If set, all image references will be prefixed with this registry. + Deprecated: Use RegistryMirrors for more flexible registry mapping. + type: string + metrics: + description: Metrics controls the enabling/disabling of metrics report + of mondoo-operator + properties: + enable: + type: boolean + resourceLabels: + additionalProperties: + type: string + description: |- + ResourceLabels allows providing a list of extra labels to apply to the metrics-related + resources (eg. ServiceMonitor) + type: object + type: object + noProxy: + description: NoProxy specifies a comma-separated list of hosts that + should not use the proxy. + type: string + registryMirrors: + additionalProperties: + type: string + description: |- + RegistryMirrors specifies a mapping of public registries to private mirrors. + The key is the public registry (e.g., "ghcr.io", "docker.io", "quay.io") + and the value is the private mirror (e.g., "artifactory.example.com/ghcr.io.docker"). + Example: + registryMirrors: + ghcr.io: artifactory.example.com/ghcr.io.docker + docker.io: artifactory.example.com/hub.docker.com + type: object + skipContainerResolution: + description: Allows skipping Image resolution from upstream repository + type: boolean + skipProxyForCnspec: + description: |- + SkipProxyForCnspec disables proxy environment variables for cnspec-based components + (scan-api, container scanning). Use this when the Mondoo API is accessible directly + without proxy (e.g., internal mirror) but other components need proxy for external access. + Default: false (proxy settings are applied to all components) + type: boolean + type: object + status: + description: MondooOperatorConfigStatus defines the observed state of + MondooOperatorConfig + properties: + conditions: + description: Conditions includes more detailed status for the mondoo + config + items: + description: Condition contains details for the current condition + of a MondooOperatorConfig + properties: + lastTransitionTime: + description: LastTransitionTime is the last time the condition + transitioned from one status to another. + format: date-time + type: string + lastUpdateTime: + description: LastUpdateTime is the last time the condition was + updated. + format: date-time + type: string + message: + description: Message is a human-readable message indicating + details about last transition. + type: string + reason: + description: Reason is a unique, one-word, CamelCase reason + for the condition's last transition. + type: string + status: + description: Status is the status of the condition. + type: string + type: + description: Type is the type of the condition. + type: string + required: + - status + - type + type: object + type: array + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/charts/mondoo-operator/templates/deployment.yaml b/charts/mondoo-operator/templates/deployment.yaml index 86bd5fa9d..ed460e8c3 100644 --- a/charts/mondoo-operator/templates/deployment.yaml +++ b/charts/mondoo-operator/templates/deployment.yaml @@ -31,6 +31,24 @@ spec: fieldPath: metadata.namespace - name: KUBERNETES_CLUSTER_DOMAIN value: {{ quote .Values.kubernetesClusterDomain }} + {{- if .Values.operator.httpProxy }} + - name: HTTP_PROXY + value: {{ .Values.operator.httpProxy | quote }} + - name: http_proxy + value: {{ .Values.operator.httpProxy | quote }} + {{- end }} + {{- if .Values.operator.httpsProxy }} + - name: HTTPS_PROXY + value: {{ .Values.operator.httpsProxy | quote }} + - name: https_proxy + value: {{ .Values.operator.httpsProxy | quote }} + {{- end }} + {{- if .Values.operator.noProxy }} + - name: NO_PROXY + value: {{ .Values.operator.noProxy | quote }} + - name: no_proxy + value: {{ .Values.operator.noProxy | quote }} + {{- end }} image: {{ .Values.controllerManager.manager.image.repository }}:{{ .Values.controllerManager.manager.image.tag | default .Chart.AppVersion }} imagePullPolicy: {{ .Values.controllerManager.manager.imagePullPolicy }} @@ -59,3 +77,7 @@ spec: 8 }} serviceAccountName: {{ include "mondoo-operator.fullname" . }}-controller-manager terminationGracePeriodSeconds: 10 + {{- if .Values.operator.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.operator.imagePullSecrets | nindent 6 }} + {{- end }} diff --git a/charts/mondoo-operator/templates/manager-rbac.yaml b/charts/mondoo-operator/templates/manager-rbac.yaml index 11e5def60..2694a98ee 100644 --- a/charts/mondoo-operator/templates/manager-rbac.yaml +++ b/charts/mondoo-operator/templates/manager-rbac.yaml @@ -36,6 +36,17 @@ rules: - create - delete - get +- apiGroups: + - "" + resources: + - serviceaccounts + verbs: + - create + - delete + - get + - list + - update + - watch - apiGroups: - apps resources: diff --git a/charts/mondoo-operator/templates/mondooauditconfig-crd.yaml b/charts/mondoo-operator/templates/mondooauditconfig-crd.yaml deleted file mode 100644 index a185dcb80..000000000 --- a/charts/mondoo-operator/templates/mondooauditconfig-crd.yaml +++ /dev/null @@ -1,1249 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: mondooauditconfigs.k8s.mondoo.com - labels: - {{- include "mondoo-operator.labels" . | nindent 4 }} -spec: - conversion: - strategy: Webhook - webhook: - clientConfig: - service: - name: webhook-service - namespace: '{{ .Release.Namespace }}' - path: /convert - conversionReviewVersions: - - v1 - group: k8s.mondoo.com - names: - kind: MondooAuditConfig - listKind: MondooAuditConfigList - plural: mondooauditconfigs - singular: mondooauditconfig - scope: Namespaced - versions: - - name: v1alpha2 - schema: - openAPIV3Schema: - description: MondooAuditConfig is the Schema for the mondooauditconfigs API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: MondooAuditConfigSpec defines the desired state of MondooAuditConfig - properties: - admission: - description: |- - Admission is DEPRECATED and ignored. Admission webhooks were removed in v12.1.0. - The operator will automatically clean up any orphaned admission resources. - See docs/admission-migration-guide.md for migration instructions. - properties: - certificateProvisioning: - description: CertificateProvisioning is DEPRECATED. - properties: - mode: - description: Mode is DEPRECATED. - type: string - type: object - enable: - description: Enable is DEPRECATED. Admission webhooks are no longer supported. - type: boolean - image: - description: Image is DEPRECATED. - properties: - digest: - description: |- - Digest specifies the image digest (e.g., sha256:abc123...). - When specified, this takes precedence over Tag. - type: string - name: - type: string - tag: - type: string - type: object - mode: - description: Mode is DEPRECATED. - type: string - replicas: - description: Replicas is DEPRECATED. - format: int32 - type: integer - serviceAccountName: - description: ServiceAccountName is DEPRECATED. - type: string - type: object - annotations: - additionalProperties: - type: string - description: |- - Annotations allows adding custom annotations to all scanned assets. These key-value pairs - will be attached to every asset discovered by the operator, making them searchable - and filterable in the Mondoo Console. - type: object - consoleIntegration: - properties: - enable: - type: boolean - type: object - containers: - properties: - enable: - type: boolean - env: - description: |- - Env allows setting extra environment variables for the node scanner. If the operator sets already an env - variable with the same name, the value specified here will override it. - items: - description: EnvVar represents an environment variable present in a Container. - properties: - name: - description: |- - Name of the environment variable. - May consist of any printable ASCII characters except '='. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - fileKeyRef: - description: |- - FileKeyRef selects a key of the env file. - Requires the EnvFiles feature gate to be enabled. - properties: - key: - description: |- - The key within the env file. An invalid key will prevent the pod from starting. - The keys defined within a source may consist of any printable ASCII characters except '='. - During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. - type: string - optional: - default: false - description: |- - Specify whether the file or its key must be defined. If the file or key - does not exist, then the env var is not published. - If optional is set to true and the specified key does not exist, - the environment variable will not be set in the Pod's containers. - - If optional is set to false and the specified key does not exist, - an error will be returned during Pod creation. - type: boolean - path: - description: |- - The path within the volume from which to select the file. - Must be relative and may not contain the '..' path or start with '..'. - type: string - volumeName: - description: The name of the volume mount containing the env file. - type: string - required: - - key - - path - - volumeName - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's namespace - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - resources: - description: ResourceRequirements describes the compute resource requirements. - 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 - schedule: - description: Specify a custom crontab schedule for the container image scanning job. If not specified, the default schedule is used. - type: string - type: object - filtering: - properties: - namespaces: - properties: - exclude: - description: |- - Exclude is the list of resources to ignore for any watching/scanning actions. Use this if - the goal is to watch/scan all resources except for this Exclude list. - items: - type: string - type: array - include: - description: |- - Include is the list of resources to watch/scan. Setting Include overrides anything in the - Exclude list as specifying an Include list is effectively excluding everything except for what - is on the Include list. - items: - type: string - type: array - type: object - type: object - kubernetesResources: - properties: - containerImageScanning: - description: |- - DEPRECATED: ContainerImageScanning determines whether container images are being scanned. The current implementation - runs a separate job once every 24h that scans the container images running in the cluster. - type: boolean - enable: - type: boolean - externalClusters: - description: |- - ExternalClusters defines remote K8s clusters to scan from this operator instance. - Each external cluster will have its own CronJob created with the appropriate kubeconfig. - items: - description: ExternalCluster defines configuration for scanning a remote K8s cluster - properties: - containerImageScanning: - description: ContainerImageScanning enables scanning of container images in this external cluster. - type: boolean - filtering: - description: |- - Filtering allows namespace filtering specific to this external cluster. - If not specified, uses the global filtering from MondooAuditConfigSpec.Filtering. - properties: - namespaces: - properties: - exclude: - description: |- - Exclude is the list of resources to ignore for any watching/scanning actions. Use this if - the goal is to watch/scan all resources except for this Exclude list. - items: - type: string - type: array - include: - description: |- - Include is the list of resources to watch/scan. Setting Include overrides anything in the - Exclude list as specifying an Include list is effectively excluding everything except for what - is on the Include list. - items: - type: string - type: array - type: object - type: object - kubeconfigSecretRef: - description: |- - KubeconfigSecretRef references a Secret containing kubeconfig for the remote cluster. - The Secret must have a key "kubeconfig" with the kubeconfig content. - Mutually exclusive with ServiceAccountAuth and WorkloadIdentity. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - name: - description: |- - Name is a unique identifier for this cluster (used in resource names). - Must be a valid Kubernetes name (lowercase, alphanumeric, hyphens allowed). - pattern: ^[a-z0-9]([-a-z0-9]*[a-z0-9])?$ - type: string - privateRegistriesPullSecretRef: - description: |- - PrivateRegistriesPullSecretRef references a Secret containing registry credentials - for pulling/scanning private images in this remote cluster. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - schedule: - description: |- - Schedule overrides the default schedule for this cluster (optional). - If not specified, uses the schedule from KubernetesResources.Schedule. - type: string - serviceAccountAuth: - description: |- - ServiceAccountAuth configures authentication using a service account token. - Mutually exclusive with KubeconfigSecretRef and WorkloadIdentity. - properties: - credentialsSecretRef: - description: |- - CredentialsSecretRef references a Secret containing: - - "token": The service account token (required) - - "ca.crt": The CA certificate (required) - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - server: - description: |- - Server is the URL of the Kubernetes API server. - Example: "https://my-cluster.example.com:6443" - pattern: ^https://.* - type: string - skipTLSVerify: - default: false - description: SkipTLSVerify skips TLS verification. NOT RECOMMENDED for production. - type: boolean - required: - - credentialsSecretRef - - server - type: object - spiffeAuth: - description: |- - SPIFFEAuth configures SPIFFE/SPIRE-based authentication using X.509 SVIDs. - Mutually exclusive with KubeconfigSecretRef, ServiceAccountAuth, and WorkloadIdentity. - properties: - audience: - description: |- - Audience is the intended audience for the SVID (optional). - Some SPIRE configurations require this for workload attestation. - type: string - server: - description: |- - Server is the URL of the remote Kubernetes API server. - Example: "https://remote-cluster.example.com:6443" - pattern: ^https://.* - type: string - socketPath: - default: /run/spire/sockets/agent.sock - description: |- - SocketPath is the path to the SPIRE agent's Workload API socket. - Defaults to "/run/spire/sockets/agent.sock" if not specified. - type: string - trustBundleSecretRef: - description: |- - TrustBundleSecretRef references a Secret containing the remote cluster's - CA certificate for TLS verification. The Secret must have a key "ca.crt". - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - required: - - server - - trustBundleSecretRef - type: object - workloadIdentity: - description: |- - WorkloadIdentity configures cloud-native Workload Identity Federation authentication. - Mutually exclusive with KubeconfigSecretRef, ServiceAccountAuth, and SPIFFEAuth. - properties: - aks: - description: |- - AKS contains AKS-specific Azure Workload Identity configuration. - Required when provider is "aks". - properties: - clientId: - description: ClientID is the Azure AD app client ID. - type: string - clusterName: - description: ClusterName is the AKS cluster name. - type: string - resourceGroup: - description: ResourceGroup containing the AKS cluster. - type: string - subscriptionId: - description: SubscriptionID is the Azure subscription. - type: string - tenantId: - description: TenantID is the Azure AD tenant ID. - type: string - required: - - clientId - - clusterName - - resourceGroup - - subscriptionId - - tenantId - type: object - eks: - description: |- - EKS contains EKS-specific IRSA configuration. - Required when provider is "eks". - properties: - clusterName: - description: ClusterName is the EKS cluster name. - type: string - region: - description: Region is the AWS region. - type: string - roleArn: - description: |- - RoleARN is the IAM role to assume. - Format: arn:aws:iam:::role/ - type: string - required: - - clusterName - - region - - roleArn - type: object - gke: - description: |- - GKE contains GKE-specific Workload Identity configuration. - Required when provider is "gke". - properties: - clusterLocation: - description: ClusterLocation is the region or zone (e.g., "us-central1" or "us-central1-a"). - type: string - clusterName: - description: ClusterName is the GKE cluster name. - type: string - googleServiceAccount: - description: |- - GoogleServiceAccount is the Google service account to impersonate. - Format: @.iam.gserviceaccount.com - type: string - projectId: - description: ProjectID is the GCP project ID. - type: string - required: - - clusterLocation - - clusterName - - googleServiceAccount - - projectId - type: object - provider: - description: Provider specifies the cloud provider for WIF. - enum: - - gke - - eks - - aks - type: string - required: - - provider - type: object - required: - - name - type: object - type: array - resourceWatcher: - description: |- - ResourceWatcher configures real-time resource watching and scanning. - When enabled, a deployment will be created that watches for K8s resource changes - and scans them immediately rather than waiting for the CronJob schedule. - properties: - debounceInterval: - default: 10s - description: |- - DebounceInterval specifies how long to batch changes before triggering a scan. - This prevents excessive scanning when multiple resources change in quick succession. - Default is 10 seconds. - type: string - enable: - description: |- - Enable enables real-time resource watching and scanning. - When enabled, a deployment will be created that watches K8s resources for changes - and scans them using cnspec. - type: boolean - minimumScanInterval: - default: 2m - description: |- - MinimumScanInterval specifies the minimum time between scans (rate limit). - This provides a hard limit on scan frequency even when resources are changing continuously. - Default is 2 minutes. - type: string - resourceTypes: - description: |- - ResourceTypes specifies which resource types to watch. If not specified, defaults are used - based on WatchAllResources setting. When WatchAllResources is false (default), defaults to: - deployments, daemonsets, statefulsets, replicasets. When true, defaults to: - pods, deployments, daemonsets, statefulsets, replicasets, jobs, cronjobs, services, ingresses, namespaces - items: - type: string - type: array - watchAllResources: - description: |- - WatchAllResources controls whether to watch all resource types or only high-priority ones. - When false (default), only watches stable workload resources: Deployments, DaemonSets, - StatefulSets, and ReplicaSets. When true, watches all resources including ephemeral ones - like Pods, Jobs, and CronJobs. - type: boolean - type: object - schedule: - description: Specify a custom crontab schedule for the Kubernetes resource scanning job. If not specified, the default schedule is used. - type: string - type: object - mondooCredsSecretRef: - description: Config is an example field of MondooAuditConfig. Edit mondooauditconfig_types.go to remove/update - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - mondooTokenSecretRef: - description: |- - MondooTokenSecretRef can optionally hold a time-limited token that the mondoo-operator will use - to create a Mondoo service account saved to the Secret specified in .spec.mondooCredsSecretRef - if that Secret does not exist. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - nodes: - properties: - enable: - type: boolean - env: - description: |- - Env allows setting extra environment variables for the node scanner. If the operator sets already an env - variable with the same name, the value specified here will override it. - items: - description: EnvVar represents an environment variable present in a Container. - properties: - name: - description: |- - Name of the environment variable. - May consist of any printable ASCII characters except '='. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - fileKeyRef: - description: |- - FileKeyRef selects a key of the env file. - Requires the EnvFiles feature gate to be enabled. - properties: - key: - description: |- - The key within the env file. An invalid key will prevent the pod from starting. - The keys defined within a source may consist of any printable ASCII characters except '='. - During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. - type: string - optional: - default: false - description: |- - Specify whether the file or its key must be defined. If the file or key - does not exist, then the env var is not published. - If optional is set to true and the specified key does not exist, - the environment variable will not be set in the Pod's containers. - - If optional is set to false and the specified key does not exist, - an error will be returned during Pod creation. - type: boolean - path: - description: |- - The path within the volume from which to select the file. - Must be relative and may not contain the '..' path or start with '..'. - type: string - volumeName: - description: The name of the volume mount containing the env file. - type: string - required: - - key - - path - - volumeName - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's namespace - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - intervalTimer: - default: 60 - description: |- - IntervalTimer is the interval (in minutes) for the node scanning. The default is "60". Only applicable for Deployment - style. - type: integer - priorityClassName: - description: PriorityClassName specifies the name of the PriorityClass for the node scanning workloads. - type: string - resources: - description: ResourceRequirements describes the compute resource requirements. - 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 - schedule: - description: |- - Schedule specifies a custom crontab schedule for the node scanning job. If not specified, the default schedule is - used. Only applicable for CronJob style - type: string - style: - default: cronjob - description: Style specifies how node scanning is deployed. The default is "cronjob" which will create a CronJob for the node scanning. - enum: - - cronjob - - deployment - - daemonset - type: string - type: object - scanner: - description: |- - Scanner defines the settings for the Mondoo scanner that will be running in the cluster. The same scanner - is used for scanning the Kubernetes API and the nodes. - properties: - env: - description: |- - Env allows setting extra environment variables for the scanner. If the operator sets already an env - variable with the same name, the value specified here will override it. - items: - description: EnvVar represents an environment variable present in a Container. - properties: - name: - description: |- - Name of the environment variable. - May consist of any printable ASCII characters except '='. - type: string - value: - description: |- - Variable references $(VAR_NAME) are expanded - using the previously defined environment variables in the container and - any service environment variables. If a variable cannot be resolved, - the reference in the input string will be unchanged. Double $$ are reduced - to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. - "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". - Escaped references will never be expanded, regardless of whether the variable - exists or not. - Defaults to "". - type: string - valueFrom: - description: Source for the environment variable's value. Cannot be used if value is not empty. - properties: - configMapKeyRef: - description: Selects a key of a ConfigMap. - properties: - key: - description: The key to select. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the ConfigMap or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - fieldRef: - description: |- - Selects a field of the pod: supports metadata.name, metadata.namespace, `metadata.labels['']`, `metadata.annotations['']`, - spec.nodeName, spec.serviceAccountName, status.hostIP, status.podIP, status.podIPs. - properties: - apiVersion: - description: Version of the schema the FieldPath is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the specified API version. - type: string - required: - - fieldPath - type: object - x-kubernetes-map-type: atomic - fileKeyRef: - description: |- - FileKeyRef selects a key of the env file. - Requires the EnvFiles feature gate to be enabled. - properties: - key: - description: |- - The key within the env file. An invalid key will prevent the pod from starting. - The keys defined within a source may consist of any printable ASCII characters except '='. - During Alpha stage of the EnvFiles feature gate, the key size is limited to 128 characters. - type: string - optional: - default: false - description: |- - Specify whether the file or its key must be defined. If the file or key - does not exist, then the env var is not published. - If optional is set to true and the specified key does not exist, - the environment variable will not be set in the Pod's containers. - - If optional is set to false and the specified key does not exist, - an error will be returned during Pod creation. - type: boolean - path: - description: |- - The path within the volume from which to select the file. - Must be relative and may not contain the '..' path or start with '..'. - type: string - volumeName: - description: The name of the volume mount containing the env file. - type: string - required: - - key - - path - - volumeName - type: object - x-kubernetes-map-type: atomic - resourceFieldRef: - description: |- - Selects a resource of the container: only resources limits and requests - (limits.cpu, limits.memory, limits.ephemeral-storage, requests.cpu, requests.memory and requests.ephemeral-storage) are currently supported. - properties: - containerName: - description: 'Container name: required for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - x-kubernetes-map-type: atomic - secretKeyRef: - description: Selects a key of a secret in the pod's namespace - properties: - key: - description: The key of the secret to select from. Must be a valid secret key. - type: string - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - optional: - description: Specify whether the Secret or its key must be defined - type: boolean - required: - - key - type: object - x-kubernetes-map-type: atomic - type: object - required: - - name - type: object - type: array - image: - properties: - digest: - description: |- - Digest specifies the image digest (e.g., sha256:abc123...). - When specified, this takes precedence over Tag. - type: string - name: - type: string - tag: - type: string - type: object - privateRegistriesPullSecretRef: - description: |- - PrivateRegistriesPullSecretRef defines the name of a secret that contains the credentials for the private - registries we have to pull images from. Use this when you have a single secret containing credentials - for one or more registries. For multiple separate secrets, use PrivateRegistriesPullSecretRefs instead. - Deprecated: Use PrivateRegistriesPullSecretRefs for new configurations. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - privateRegistriesPullSecretRefs: - description: |- - PrivateRegistriesPullSecretRefs defines a list of secrets that contain credentials for private - registries. Use this when you need to pull images from multiple private registries and the credentials - are stored in separate secrets (e.g., managed by different teams or external secret operators). - The credentials from all secrets will be merged. If both PrivateRegistriesPullSecretRef and - PrivateRegistriesPullSecretRefs are specified, all secrets will be merged together. - items: - description: |- - LocalObjectReference contains enough information to let you locate the - referenced object inside the same namespace. - properties: - name: - default: "" - description: |- - Name of the referent. - This field is effectively required, but due to backwards compatibility is - allowed to be empty. Instances of this type with an empty value here are - almost certainly wrong. - More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names - type: string - type: object - x-kubernetes-map-type: atomic - type: array - replicas: - default: 1 - description: |- - Number of replicas for the scanner. - For enforcing mode, the minimum should be two to prevent problems during Pod failures, - e.g. node failure, node scaling, etc. - format: int32 - minimum: 1 - type: integer - resources: - description: ResourceRequirements describes the compute resource requirements. - 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 - serviceAccountName: - default: mondoo-operator-k8s-resources-scanning - type: string - type: object - required: - - mondooCredsSecretRef - type: object - status: - description: MondooAuditConfigStatus defines the observed state of MondooAuditConfig - properties: - conditions: - description: Conditions includes detailed status for the MondooAuditConfig - items: - properties: - affectedPods: - description: AffectedPods, when filled, contains a list which are affected by an issue - items: - type: string - type: array - lastTransitionTime: - description: LastTransitionTime is the last time the condition transitioned from one status to another. - format: date-time - type: string - lastUpdateTime: - description: LastUpdateTime is the last time we probed the condition - format: date-time - type: string - memoryLimit: - description: MemoryLimit contains the currently active memory limit for a Pod - type: string - message: - description: Message is a human-readable message indicating details about the last transition - type: string - reason: - description: Reason is a unique, one-word, CamelCase reason for the condition's last transition - type: string - status: - description: Status is the status of the condition - type: string - type: - description: Type is the specific type of the condition - type: string - required: - - status - - type - type: object - type: array - pods: - description: Pods store the name of the pods which are running mondoo instances - items: - type: string - type: array - reconciledByOperatorVersion: - description: ReconciledByOperatorVersion contains the version of the operator which reconciled this MondooAuditConfig - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/charts/mondoo-operator/templates/mondoooperatorconfig-crd.yaml b/charts/mondoo-operator/templates/mondoooperatorconfig-crd.yaml deleted file mode 100644 index 493558df7..000000000 --- a/charts/mondoo-operator/templates/mondoooperatorconfig-crd.yaml +++ /dev/null @@ -1,104 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.20.0 - name: mondoooperatorconfigs.k8s.mondoo.com - labels: - {{- include "mondoo-operator.labels" . | nindent 4 }} -spec: - group: k8s.mondoo.com - names: - kind: MondooOperatorConfig - listKind: MondooOperatorConfigList - plural: mondoooperatorconfigs - singular: mondoooperatorconfig - scope: Cluster - versions: - - name: v1alpha2 - schema: - openAPIV3Schema: - description: MondooOperatorConfig is the Schema for the mondoooperatorconfigs API - properties: - apiVersion: - description: |- - APIVersion defines the versioned schema of this representation of an object. - Servers should convert recognized schemas to the latest internal value, and - may reject unrecognized values. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources - type: string - kind: - description: |- - Kind is a string value representing the REST resource this object represents. - Servers may infer this from the endpoint the client submits requests to. - Cannot be updated. - In CamelCase. - More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds - type: string - metadata: - type: object - spec: - description: MondooOperatorConfigSpec defines the desired state of MondooOperatorConfig - properties: - containerProxy: - description: ContainerProxy specifies a proxy to use for container images. - type: string - httpProxy: - description: HttpProxy specifies a proxy to use for HTTP requests to the Mondoo Platform. - type: string - metrics: - description: Metrics controls the enabling/disabling of metrics report of mondoo-operator - properties: - enable: - type: boolean - resourceLabels: - additionalProperties: - type: string - description: |- - ResourceLabels allows providing a list of extra labels to apply to the metrics-related - resources (eg. ServiceMonitor) - type: object - type: object - skipContainerResolution: - description: Allows skipping Image resolution from upstream repository - type: boolean - type: object - status: - description: MondooOperatorConfigStatus defines the observed state of MondooOperatorConfig - properties: - conditions: - description: Conditions includes more detailed status for the mondoo config - items: - description: Condition contains details for the current condition of a MondooOperatorConfig - properties: - lastTransitionTime: - description: LastTransitionTime is the last time the condition transitioned from one status to another. - format: date-time - type: string - lastUpdateTime: - description: LastUpdateTime is the last time the condition was updated. - format: date-time - type: string - message: - description: Message is a human-readable message indicating details about last transition. - type: string - reason: - description: Reason is a unique, one-word, CamelCase reason for the condition's last transition. - type: string - status: - description: Status is the status of the condition. - type: string - type: - description: Type is the type of the condition. - type: string - required: - - status - - type - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/charts/mondoo-operator/templates/mondoooperatorconfig.yaml b/charts/mondoo-operator/templates/mondoooperatorconfig.yaml new file mode 100644 index 000000000..e794ee3b6 --- /dev/null +++ b/charts/mondoo-operator/templates/mondoooperatorconfig.yaml @@ -0,0 +1,38 @@ +{{- if .Values.operator.createConfig }} +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config + labels: + {{- include "mondoo-operator.labels" . | nindent 4 }} +spec: + {{- if .Values.operator.httpProxy }} + httpProxy: {{ .Values.operator.httpProxy | quote }} + {{- end }} + {{- if .Values.operator.httpsProxy }} + httpsProxy: {{ .Values.operator.httpsProxy | quote }} + {{- end }} + {{- if .Values.operator.noProxy }} + noProxy: {{ .Values.operator.noProxy | quote }} + {{- end }} + {{- if .Values.operator.containerProxy }} + containerProxy: {{ .Values.operator.containerProxy | quote }} + {{- end }} + {{- if .Values.operator.imagePullSecrets }} + imagePullSecrets: + {{- toYaml .Values.operator.imagePullSecrets | nindent 4 }} + {{- end }} + {{- if .Values.operator.imageRegistry }} + imageRegistry: {{ .Values.operator.imageRegistry | quote }} + {{- end }} + {{- if .Values.operator.registryMirrors }} + registryMirrors: + {{- toYaml .Values.operator.registryMirrors | nindent 4 }} + {{- end }} + {{- if .Values.operator.skipContainerResolution }} + skipContainerResolution: {{ .Values.operator.skipContainerResolution }} + {{- end }} + {{- if .Values.operator.skipProxyForCnspec }} + skipProxyForCnspec: {{ .Values.operator.skipProxyForCnspec }} + {{- end }} +{{- end }} diff --git a/charts/mondoo-operator/values.yaml b/charts/mondoo-operator/values.yaml index 437c17f4a..fe2606111 100644 --- a/charts/mondoo-operator/values.yaml +++ b/charts/mondoo-operator/values.yaml @@ -60,3 +60,46 @@ cleanup: enabled: true # Timeout for waiting for MondooAuditConfig resources to be deleted timeout: 2m + +# Mondoo Operator Configuration +# These settings are applied to the MondooOperatorConfig custom resource +operator: + # Create MondooOperatorConfig resource + # Set to false to skip creating the MondooOperatorConfig CR (e.g., if managing it separately) + createConfig: true + # HTTP proxy for outbound connections to Mondoo Platform + httpProxy: "" + # HTTPS proxy for outbound connections to Mondoo Platform + httpsProxy: "" + # Comma-separated list of hosts that should bypass the proxy + noProxy: "" + # Container proxy for pulling container images (used in container image scanning) + containerProxy: "" + # Image pull secrets for pulling Mondoo images from private registries + # Example: + # imagePullSecrets: + # - name: my-registry-secret + imagePullSecrets: [] + # Custom image registry prefix for simple registry mirror setups + # Use this when all Mondoo images should be pulled from the same mirror + # Example: "registry.example.com/ghcr.io.docker" + # This will rewrite image references like: + # ghcr.io/mondoohq/mondoo-operator:v1.0.0 -> registry.example.com/ghcr.io.docker/mondoohq/mondoo-operator:v1.0.0 + imageRegistry: "" + # Registry mirrors for mapping multiple public registries to private mirrors + # Use this when you need different mirrors for different source registries + # Example: + # registryMirrors: + # ghcr.io: registry.example.com/ghcr.io.docker + # docker.io: registry.example.com/hub.docker.com + # quay.io: registry.example.com/quay.io + # Note: If both imageRegistry and registryMirrors are set, registryMirrors takes precedence + registryMirrors: {} + + # Skip proxy settings for cnspec-based components (scan-api, container scanning) + # Set to true when using an internal Mondoo API mirror that doesn't require proxy + # but other components still need proxy for external access (e.g., image resolution) + # Default: false (proxy settings are applied to all components) + skipProxyForCnspec: false + # Skip container image resolution (useful for air-gapped environments) + skipContainerResolution: false diff --git a/cmd/mondoo-operator/operator/operator_status.go b/cmd/mondoo-operator/operator/operator_status.go index 78af7e594..08e4dfb8e 100644 --- a/cmd/mondoo-operator/operator/operator_status.go +++ b/cmd/mondoo-operator/operator/operator_status.go @@ -31,7 +31,7 @@ func checkForTerminatedState(ctx context.Context, nonCacheClient client.Client, if errors.IsNotFound(err) { logger.Info("MondooOperatorConfig not found, using defaults") } else { - logger.Error(err, "Failed to check for MondooOpertorConfig") + logger.Error(err, "Failed to check for MondooOperatorConfig") return err } } diff --git a/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml b/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml index 2958ed4b1..49622c75a 100644 --- a/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml +++ b/config/crd/bases/k8s.mondoo.com_mondoooperatorconfigs.yaml @@ -48,6 +48,38 @@ spec: description: HttpProxy specifies a proxy to use for HTTP requests to the Mondoo Platform. type: string + httpsProxy: + description: HttpsProxy specifies a proxy to use for HTTPS requests + to the Mondoo Platform. + type: string + imagePullSecrets: + description: |- + ImagePullSecrets specifies the name of the Secret to use for pulling images for all Mondoo components. + The secret must be of type kubernetes.io/dockerconfigjson. + items: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + default: "" + description: |- + Name of the referent. + This field is effectively required, but due to backwards compatibility is + allowed to be empty. Instances of this type with an empty value here are + almost certainly wrong. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + type: array + imageRegistry: + description: |- + ImageRegistry specifies a custom container image registry prefix for all Mondoo images. + Use this for simple registry mirrors where all images go to the same mirror. + Example: "artifactory.example.com/ghcr.io.docker" + For more complex setups with multiple source registries, use RegistryMirrors instead. + type: string metrics: description: Metrics controls the enabling/disabling of metrics report of mondoo-operator @@ -62,9 +94,34 @@ spec: resources (eg. ServiceMonitor) type: object type: object + noProxy: + description: NoProxy specifies a comma-separated list of hosts that + should not use the proxy. + type: string + registryMirrors: + additionalProperties: + type: string + description: |- + RegistryMirrors specifies a mapping of public registries to private mirrors. + Use this when you need to map different source registries to different mirrors. + The key is the public registry (e.g., "ghcr.io", "docker.io", "quay.io") + and the value is the private mirror (e.g., "artifactory.example.com/ghcr.io.docker"). + Example: + registryMirrors: + ghcr.io: artifactory.example.com/ghcr.io.docker + docker.io: artifactory.example.com/hub.docker.com + Note: If both ImageRegistry and RegistryMirrors are set, RegistryMirrors takes precedence. + type: object skipContainerResolution: description: Allows skipping Image resolution from upstream repository type: boolean + skipProxyForCnspec: + description: |- + SkipProxyForCnspec disables proxy environment variables for cnspec-based components + (scan-api, container scanning). Use this when the Mondoo API is accessible directly + without proxy (e.g., internal mirror) but other components need proxy for external access. + Default: false (proxy settings are applied to all components) + type: boolean type: object status: description: MondooOperatorConfigStatus defines the observed state of diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index dd5d79afe..a264b568c 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -85,6 +85,7 @@ rules: resources: - jobs verbs: + - delete - deletecollection - get - list diff --git a/config/samples/k8s_v1alpha2_mondoooperatorconfig.yaml b/config/samples/k8s_v1alpha2_mondoooperatorconfig.yaml index 042190001..97b6e9beb 100644 --- a/config/samples/k8s_v1alpha2_mondoooperatorconfig.yaml +++ b/config/samples/k8s_v1alpha2_mondoooperatorconfig.yaml @@ -1,11 +1,83 @@ # Copyright (c) Mondoo, Inc. # SPDX-License-Identifier: BUSL-1.1 +# MondooOperatorConfig defines cluster-wide settings for the Mondoo Operator. +# This is a cluster-scoped resource - only one instance named "mondoo-operator-config" is allowed. +# See docs/operator-config.md for detailed documentation. + apiVersion: k8s.mondoo.com/v1alpha2 kind: MondooOperatorConfig metadata: name: mondoo-operator-config spec: + # ============================================================================ + # METRICS CONFIGURATION + # ============================================================================ + metrics: + # Enable Prometheus metrics and ServiceMonitor creation enable: false + + # Extra labels to add to metrics-related resources (e.g., ServiceMonitor) + # Use this to match your Prometheus Operator's serviceMonitorSelector + # resourceLabels: + # prometheus: main + # team: security + + # ============================================================================ + # PROXY CONFIGURATION + # ============================================================================ + + # HTTP proxy for outbound HTTP connections to the Mondoo Platform + # httpProxy: "http://proxy.example.com:3128" + + # HTTPS proxy for outbound HTTPS connections to the Mondoo Platform + # httpsProxy: "http://proxy.example.com:3128" + + # Comma-separated list of hosts/CIDRs that should bypass the proxy + # Follows standard NO_PROXY conventions: IPs, CIDRs, domains, domain suffixes (.example.com) + # Recommended entries for Kubernetes: + # noProxy: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,.cluster.local,.svc,localhost,127.0.0.1" + + # Proxy specifically for container image operations (used in container image scanning) + # containerProxy: "http://proxy.example.com:3128" + + # Disable proxy environment variables for cnspec-based components (K8s scanning, container scanning) + # Use this when the Mondoo API is accessible directly without proxy (e.g., internal mirror) + # but other components still need proxy for external access + skipProxyForCnspec: false + + # ============================================================================ + # REGISTRY CONFIGURATION (for air-gapped / private registry environments) + # ============================================================================ + + # Image pull secrets for pulling Mondoo container images from private registries + # The secrets must exist in the mondoo-operator namespace and be of type kubernetes.io/dockerconfigjson + # imagePullSecrets: + # - name: ghcr-credentials + # - name: artifactory-credentials + + # Simple registry mirror: prefix for all Mondoo images + # Use this when all images are mirrored to the same registry location + # Example: "registry.example.com/ghcr.io.docker" + # This rewrites: ghcr.io/mondoohq/cnspec:v1.0.0 -> registry.example.com/ghcr.io.docker/mondoohq/cnspec:v1.0.0 + # imageRegistry: "" + + # Advanced registry mirrors: map different source registries to different mirrors + # Use this when you have separate mirrors for different upstream registries + # Note: If both imageRegistry and registryMirrors are set, registryMirrors takes precedence + # registryMirrors: + # ghcr.io: "artifactory.example.com/ghcr-remote" + # docker.io: "artifactory.example.com/docker-hub-remote" + # quay.io: "artifactory.example.com/quay-remote" + + # ============================================================================ + # BEHAVIOR FLAGS + # ============================================================================ + + # Skip resolving container image digests from upstream registries + # Enable this for: + # - Air-gapped clusters without access to ghcr.io + # - GKE Autopilot or environments with restricted network policies + # - When you want to pin exact image versions without upstream resolution skipContainerResolution: false diff --git a/controllers/container_image/resources.go b/controllers/container_image/resources.go index 5080b184a..41deece82 100644 --- a/controllers/container_image/resources.go +++ b/controllers/container_image/resources.go @@ -38,12 +38,22 @@ func CronJob(image, integrationMrn, clusterUid, privateRegistrySecretName string "--inventory-file", "/etc/opt/mondoo/config/inventory.yml", } - if cfg.Spec.HttpProxy != nil { - cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) + // Only add proxy settings if SkipProxyForCnspec is false + // cnspec-based components may not properly handle NO_PROXY for internal domains + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } } envVars := feature_flags.AllFeatureFlagsAsEnv() envVars = append(envVars, corev1.EnvVar{Name: "MONDOO_AUTO_UPDATE", Value: "false"}) + + // Add proxy environment variables from MondooOperatorConfig only if SkipProxyForCnspec is false + if !cfg.Spec.SkipProxyForCnspec { + envVars = append(envVars, k8s.ProxyEnvVars(cfg)...) + } + envVars = k8s.MergeEnv(envVars, m.Spec.Containers.Env) cronjob := &batchv1.CronJob{ @@ -152,6 +162,13 @@ func CronJob(image, integrationMrn, clusterUid, privateRegistrySecretName string // Add private registry secret if specified k8s.AddPrivateRegistryPullSecretToSpec(&cronjob.Spec.JobTemplate.Spec.Template.Spec, privateRegistrySecretName) + // Append imagePullSecrets from MondooOperatorConfig (don't overwrite existing secrets) + if len(cfg.Spec.ImagePullSecrets) > 0 { + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = append( + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) + } + return cronjob } diff --git a/controllers/container_image/resources_test.go b/controllers/container_image/resources_test.go index 825c42226..13e7efffa 100644 --- a/controllers/container_image/resources_test.go +++ b/controllers/container_image/resources_test.go @@ -4,12 +4,15 @@ package container_image import ( + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory" "go.mondoo.com/mondoo-operator/api/v1alpha2" @@ -17,6 +20,20 @@ import ( const testClusterUID = "abcdefg" +func testAuditConfig() *v1alpha2.MondooAuditConfig { + return &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mondoo-client", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + Containers: v1alpha2.Containers{ + Schedule: "0 * * * *", + }, + }, + } +} + func TestInventory_WithAnnotations(t *testing.T) { auditConfig := v1alpha2.MondooAuditConfig{ ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, @@ -40,3 +57,148 @@ func TestInventory_WithAnnotations(t *testing.T) { assert.Equal(t, "platform", asset.Annotations["team"], "asset %s missing team annotation", asset.Name) } } + +func TestCronJob_WithProxy(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + cj := CronJob("test-image:latest", "", testClusterUID, "", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy") + assert.Contains(t, cmdStr, "https://proxy:8443") + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) +} + +func TestCronJob_SkipProxyForCnspec(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + SkipProxyForCnspec: true, + }, + } + + cj := CronJob("test-image:latest", "", testClusterUID, "", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + _, hasHTTPSProxy := envMap["HTTPS_PROXY"] + assert.False(t, hasHTTPProxy, "HTTP_PROXY should not be set when SkipProxyForCnspec is true") + assert.False(t, hasHTTPSProxy, "HTTPS_PROXY should not be set when SkipProxyForCnspec is true") +} + +func TestCronJob_WithImagePullSecrets(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + cj := CronJob("test-image:latest", "", testClusterUID, "", m, cfg) + secrets := cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +func TestCronJob_ImagePullSecrets_AppendsMultiple(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "secret-one"}, + {Name: "secret-two"}, + }, + }, + } + + cj := CronJob("test-image:latest", "", testClusterUID, "", m, cfg) + secrets := cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets + + require.Len(t, secrets, 2) + assert.Equal(t, "secret-one", secrets[0].Name) + assert.Equal(t, "secret-two", secrets[1].Name) +} + +func TestCronJob_PrivateRegistrySecretMountsDockerConfig(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{} + + cj := CronJob("test-image:latest", "", testClusterUID, "private-registry-secret", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + // Private registry secret should be mounted as a Docker config volume, not as ImagePullSecrets + envMap := envToMap(container.Env) + assert.Equal(t, "/etc/opt/mondoo/docker", envMap["DOCKER_CONFIG"]) + + found := false + for _, vm := range container.VolumeMounts { + if vm.Name == "pull-secrets" { + found = true + assert.Equal(t, "/etc/opt/mondoo/docker", vm.MountPath) + } + } + assert.True(t, found, "pull-secrets volume mount should be present") +} + +func TestInventory_WithContainerProxy(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + } + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ContainerProxy: ptr.To("http://container-proxy:3128"), + }, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, cfg) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + assert.Equal(t, "http://container-proxy:3128", inv.Spec.Assets[0].Connections[0].Options["container-proxy"]) +} + +func TestInventory_WithoutContainerProxy(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{}) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + _, hasContainerProxy := inv.Spec.Assets[0].Connections[0].Options["container-proxy"] + assert.False(t, hasContainerProxy) +} + +// envToMap converts a slice of EnvVar to a map for easy lookup. +func envToMap(envVars []corev1.EnvVar) map[string]string { + m := make(map[string]string, len(envVars)) + for _, e := range envVars { + m[e.Name] = e.Value + } + return m +} diff --git a/controllers/integration/integration_controller.go b/controllers/integration/integration_controller.go index ada9cb74f..24ad76c44 100644 --- a/controllers/integration/integration_controller.go +++ b/controllers/integration/integration_controller.go @@ -123,7 +123,7 @@ func (r *IntegrationReconciler) processMondooAuditConfig(m v1alpha2.MondooAuditC } } - if err = mondoo.IntegrationCheckIn(r.ctx, integrationMrn, *serviceAccount, r.MondooClientBuilder, config.Spec.HttpProxy, logger); err != nil { + if err = mondoo.IntegrationCheckIn(r.ctx, integrationMrn, *serviceAccount, r.MondooClientBuilder, config.Spec.HttpProxy, config.Spec.HttpsProxy, config.Spec.NoProxy, logger); err != nil { logger.Error(err, "failed to CheckIn() for integration", "integrationMRN", string(integrationMrn)) return err } diff --git a/controllers/k8s_scan/deployment_handler.go b/controllers/k8s_scan/deployment_handler.go index 4ef301259..e52a05553 100644 --- a/controllers/k8s_scan/deployment_handler.go +++ b/controllers/k8s_scan/deployment_handler.go @@ -583,6 +583,8 @@ func (n *DeploymentHandler) performGarbageCollection(ctx context.Context, manage } if n.MondooOperatorConfig != nil { opts.HttpProxy = n.MondooOperatorConfig.Spec.HttpProxy + opts.HttpsProxy = n.MondooOperatorConfig.Spec.HttpsProxy + opts.NoProxy = n.MondooOperatorConfig.Spec.NoProxy } mondooClient, err := n.MondooClientBuilder(opts) diff --git a/controllers/k8s_scan/resources.go b/controllers/k8s_scan/resources.go index 1578e7b01..f7bde9df0 100644 --- a/controllers/k8s_scan/resources.go +++ b/controllers/k8s_scan/resources.go @@ -59,11 +59,14 @@ func CronJob(image string, m *v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOpe "--score-threshold", "0", } - if cfg.Spec.HttpProxy != nil { - cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) + // Only add proxy if configured and not skipped for cnspec + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } } - envVars := feature_flags.AllFeatureFlagsAsEnv() + envVars := buildEnvVars(cfg) envVars = append(envVars, corev1.EnvVar{Name: "MONDOO_AUTO_UPDATE", Value: "false"}) cronjob := &batchv1.CronJob{ @@ -169,6 +172,13 @@ func CronJob(image string, m *v1alpha2.MondooAuditConfig, cfg v1alpha2.MondooOpe }, } + // Add imagePullSecrets from MondooOperatorConfig + if len(cfg.Spec.ImagePullSecrets) > 0 { + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = append( + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) + } + return cronjob } @@ -183,11 +193,14 @@ func ExternalClusterCronJob(image string, cluster v1alpha2.ExternalCluster, m *v "--score-threshold", "0", } - if cfg.Spec.HttpProxy != nil { - cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) + // Only add proxy if configured and not skipped for cnspec + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } } - envVars := feature_flags.AllFeatureFlagsAsEnv() + envVars := buildEnvVars(cfg) envVars = append(envVars, corev1.EnvVar{Name: "MONDOO_AUTO_UPDATE", Value: "false"}) // Point KUBECONFIG to the mounted kubeconfig file envVars = append(envVars, corev1.EnvVar{Name: "KUBECONFIG", Value: "/etc/opt/mondoo/kubeconfig/kubeconfig"}) @@ -447,6 +460,13 @@ func ExternalClusterCronJob(image string, cluster v1alpha2.ExternalCluster, m *v }, } + // Add imagePullSecrets from MondooOperatorConfig + if len(cfg.Spec.ImagePullSecrets) > 0 { + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = append( + cronjob.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) + } + // Add private registry pull secrets if configured if cluster.PrivateRegistriesPullSecretRef != nil && cluster.PrivateRegistriesPullSecretRef.Name != "" { cronjob.Spec.JobTemplate.Spec.Template.Spec.Volumes = append(cronjob.Spec.JobTemplate.Spec.Template.Spec.Volumes, corev1.Volume{ @@ -1021,3 +1041,14 @@ func ExternalClusterInventory(integrationMRN, operatorClusterUID string, cluster return string(invBytes), nil } + +func buildEnvVars(cfg v1alpha2.MondooOperatorConfig) []corev1.EnvVar { + envVars := feature_flags.AllFeatureFlagsAsEnv() + + // Add proxy environment variables only if not skipped for cnspec components + if !cfg.Spec.SkipProxyForCnspec { + envVars = append(envVars, k8s.ProxyEnvVars(cfg)...) + } + + return envVars +} diff --git a/controllers/k8s_scan/resources_test.go b/controllers/k8s_scan/resources_test.go index 63ad697e7..3f37c7b7e 100644 --- a/controllers/k8s_scan/resources_test.go +++ b/controllers/k8s_scan/resources_test.go @@ -4,12 +4,15 @@ package k8s_scan import ( + "strings" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "gopkg.in/yaml.v2" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "go.mondoo.com/cnquery/v12/providers-sdk/v1/inventory" "go.mondoo.com/mondoo-operator/api/v1alpha2" @@ -17,6 +20,20 @@ import ( const testClusterUID = "abcdefg" +func testAuditConfig() *v1alpha2.MondooAuditConfig { + return &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "mondoo-client", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + KubernetesResources: v1alpha2.KubernetesResources{ + Schedule: "0 * * * *", + }, + }, + } +} + func TestInventory_WithAnnotations(t *testing.T) { auditConfig := v1alpha2.MondooAuditConfig{ ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, @@ -68,3 +85,221 @@ func TestExternalClusterInventory_WithAnnotations(t *testing.T) { assert.Equal(t, "security", asset.Annotations["team"], "asset %s missing team annotation", asset.Name) } } + +func TestCronJob_WithProxy(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + cj := CronJob("test-image:latest", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy") + assert.Contains(t, cmdStr, "https://proxy:8443") + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) +} + +func TestCronJob_HttpsProxyPreferred(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + cj := CronJob("test-image:latest", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy https://proxy:8443") + assert.NotContains(t, cmdStr, "http://proxy:8080") +} + +func TestCronJob_SkipProxyForCnspec(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + SkipProxyForCnspec: true, + }, + } + + cj := CronJob("test-image:latest", m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + _, hasHTTPSProxy := envMap["HTTPS_PROXY"] + assert.False(t, hasHTTPProxy, "HTTP_PROXY should not be set when SkipProxyForCnspec is true") + assert.False(t, hasHTTPSProxy, "HTTPS_PROXY should not be set when SkipProxyForCnspec is true") +} + +func TestCronJob_WithImagePullSecrets(t *testing.T) { + m := testAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + cj := CronJob("test-image:latest", m, cfg) + secrets := cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +func TestExternalClusterCronJob_WithProxy(t *testing.T) { + m := testAuditConfig() + cluster := v1alpha2.ExternalCluster{ + Name: "remote", + KubeconfigSecretRef: &corev1.LocalObjectReference{ + Name: "kubeconfig-secret", + }, + } + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + cj := ExternalClusterCronJob("test-image:latest", cluster, m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy") + assert.Contains(t, cmdStr, "https://proxy:8443") + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) +} + +func TestExternalClusterCronJob_SkipProxy(t *testing.T) { + m := testAuditConfig() + cluster := v1alpha2.ExternalCluster{ + Name: "remote", + KubeconfigSecretRef: &corev1.LocalObjectReference{ + Name: "kubeconfig-secret", + }, + } + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + SkipProxyForCnspec: true, + }, + } + + cj := ExternalClusterCronJob("test-image:latest", cluster, m, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + assert.False(t, hasHTTPProxy) +} + +func TestExternalClusterCronJob_ImagePullSecrets(t *testing.T) { + m := testAuditConfig() + cluster := v1alpha2.ExternalCluster{ + Name: "remote", + KubeconfigSecretRef: &corev1.LocalObjectReference{ + Name: "kubeconfig-secret", + }, + } + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + cj := ExternalClusterCronJob("test-image:latest", cluster, m, cfg) + secrets := cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +func TestInventory_WithContainerProxy(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + } + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ContainerProxy: ptr.To("http://container-proxy:3128"), + }, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, cfg) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + assert.Equal(t, "http://container-proxy:3128", inv.Spec.Assets[0].Connections[0].Options["container-proxy"]) +} + +func TestInventory_WithoutContainerProxy(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + } + + invStr, err := Inventory("", testClusterUID, auditConfig, v1alpha2.MondooOperatorConfig{}) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + _, hasContainerProxy := inv.Spec.Assets[0].Connections[0].Options["container-proxy"] + assert.False(t, hasContainerProxy) +} + +func TestExternalClusterInventory_WithContainerProxy(t *testing.T) { + auditConfig := v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{Name: "mondoo-client"}, + } + cluster := v1alpha2.ExternalCluster{Name: "remote-cluster"} + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ContainerProxy: ptr.To("http://container-proxy:3128"), + }, + } + + invStr, err := ExternalClusterInventory("", testClusterUID, cluster, auditConfig, cfg) + require.NoError(t, err) + + var inv inventory.Inventory + require.NoError(t, yaml.Unmarshal([]byte(invStr), &inv)) + require.NotEmpty(t, inv.Spec.Assets) + + assert.Equal(t, "http://container-proxy:3128", inv.Spec.Assets[0].Connections[0].Options["container-proxy"]) +} + +// envToMap converts a slice of EnvVar to a map for easy lookup. +func envToMap(envVars []corev1.EnvVar) map[string]string { + m := make(map[string]string, len(envVars)) + for _, e := range envVars { + m[e.Name] = e.Value + } + return m +} diff --git a/controllers/mondooauditconfig_controller.go b/controllers/mondooauditconfig_controller.go index 5406773f9..2c386601f 100644 --- a/controllers/mondooauditconfig_controller.go +++ b/controllers/mondooauditconfig_controller.go @@ -63,7 +63,7 @@ var MondooClientBuilder = mondooclient.NewClient //+kubebuilder:rbac:groups=apps,resources=deployments;daemonsets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=apps,resources=deployments;replicasets;daemonsets;statefulsets,verbs=get;list;watch //+kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete -//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=deletecollection +//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=delete;deletecollection //+kubebuilder:rbac:groups=batch,resources=cronjobs;jobs,verbs=get;list;watch //+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=core,resources=pods;namespaces;nodes,verbs=get;list;watch @@ -111,7 +111,7 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re if errors.IsNotFound(reconcileError) { log.Info("MondooOperatorConfig not found, using defaults") } else { - log.Error(reconcileError, "Failed to check for MondooOpertorConfig") + log.Error(reconcileError, "Failed to check for MondooOperatorConfig") return ctrl.Result{}, reconcileError } } @@ -131,6 +131,18 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re config = &v1alpha2.MondooOperatorConfig{} } + // Apply registry configuration to the container image resolver + imageResolver := r.ContainerImageResolver + if len(config.Spec.RegistryMirrors) > 0 { + imageResolver = imageResolver.WithRegistryMirrors(config.Spec.RegistryMirrors) + } else if config.Spec.ImageRegistry != nil && *config.Spec.ImageRegistry != "" { + imageResolver = imageResolver.WithImageRegistry(*config.Spec.ImageRegistry) + } + // Apply imagePullSecrets for authentication when resolving images + if len(config.Spec.ImagePullSecrets) > 0 { + imageResolver = imageResolver.WithImagePullSecrets(config.Spec.ImagePullSecrets) + } + if !mondooAuditConfig.DeletionTimestamp.IsZero() { log.Info("deleting") @@ -262,7 +274,7 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re Mondoo: mondooAuditConfig, KubeClient: r.Client, MondooOperatorConfig: config, - ContainerImageResolver: r.ContainerImageResolver, + ContainerImageResolver: imageResolver, IsOpenshift: r.RunningOnOpenShift, } @@ -277,7 +289,7 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re containers := container_image.DeploymentHandler{ Mondoo: mondooAuditConfig, KubeClient: r.Client, - ContainerImageResolver: r.ContainerImageResolver, + ContainerImageResolver: imageResolver, MondooOperatorConfig: config, } @@ -293,7 +305,7 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re Mondoo: mondooAuditConfig, KubeClient: r.Client, MondooOperatorConfig: config, - ContainerImageResolver: r.ContainerImageResolver, + ContainerImageResolver: imageResolver, MondooClientBuilder: r.MondooClientBuilder, } @@ -309,7 +321,7 @@ func (r *MondooAuditConfigReconciler) Reconcile(ctx context.Context, req ctrl.Re Mondoo: mondooAuditConfig, KubeClient: r.Client, MondooOperatorConfig: config, - ContainerImageResolver: r.ContainerImageResolver, + ContainerImageResolver: imageResolver, } result, reconcileError = resourceWatcher.Reconcile(ctx) @@ -490,9 +502,28 @@ func (r *MondooAuditConfigReconciler) exchangeTokenForServiceAccount(ctx context client.ObjectKeyFromObject(mondooCredsSecret), tokenData, cfg.Spec.HttpProxy, + cfg.Spec.HttpsProxy, + cfg.Spec.NoProxy, log) } +// operatorConfigRequestMapper maps MondooOperatorConfig changes to enqueue all MondooAuditConfigs +// for reconciliation, so proxy/registry changes take effect without waiting for the next scheduled reconcile. +func (r *MondooAuditConfigReconciler) operatorConfigRequestMapper(ctx context.Context, o client.Object) []reconcile.Request { + var requests []reconcile.Request + auditConfigs := &v1alpha2.MondooAuditConfigList{} + if err := r.List(ctx, auditConfigs); err != nil { + logger := ctrllog.Log.WithName("operator-config-watcher") + logger.Error(err, "Failed to list MondooAuditConfigs") + return requests + } + + for _, a := range auditConfigs.Items { + requests = append(requests, reconcile.Request{NamespacedName: client.ObjectKeyFromObject(&a)}) + } + return requests +} + // SetupWithManager sets up the controller with the Manager. func (r *MondooAuditConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). @@ -507,6 +538,9 @@ func (r *MondooAuditConfigReconciler) SetupWithManager(mgr ctrl.Manager) error { &corev1.Node{}, handler.EnqueueRequestsFromMapFunc(r.nodeEventsRequestMapper), builder.WithPredicates(k8s.IgnoreGenericEventsPredicate{})). + Watches( + &v1alpha2.MondooOperatorConfig{}, + handler.EnqueueRequestsFromMapFunc(r.operatorConfigRequestMapper)). Complete(r) } diff --git a/controllers/nodes/resources.go b/controllers/nodes/resources.go index dfea08149..8d7e0e782 100644 --- a/controllers/nodes/resources.go +++ b/controllers/nodes/resources.go @@ -46,14 +46,22 @@ func CronJob(image string, node corev1.Node, m *v1alpha2.MondooAuditConfig, isOp "--inventory-template", "/etc/opt/mondoo/inventory_template.yml", } - if cfg.Spec.HttpProxy != nil { - cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) + // Add API proxy if configured (respect SkipProxyForCnspec since node scanning uses cnspec) + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } + } + + var proxyEnvVars []corev1.EnvVar + if !cfg.Spec.SkipProxyForCnspec { + proxyEnvVars = k8s.ProxyEnvVars(cfg) } containerResources := k8s.ResourcesRequirementsWithDefaults(m.Spec.Nodes.Resources, k8s.DefaultNodeScanningResources) gcLimit := gomemlimit.CalculateGoMemLimit(containerResources) - return &batchv1.CronJob{ + cj := &batchv1.CronJob{ ObjectMeta: metav1.ObjectMeta{ Name: CronJobName(m.Name, node.Name), Namespace: m.Namespace, @@ -114,13 +122,13 @@ func CronJob(image string, node corev1.Node, m *v1alpha2.MondooAuditConfig, isOp {Name: "config", ReadOnly: true, MountPath: "/etc/opt/"}, {Name: "temp", MountPath: "/tmp"}, }, - Env: k8s.MergeEnv([]corev1.EnvVar{ + Env: k8s.MergeEnv(append([]corev1.EnvVar{ {Name: "DEBUG", Value: "false"}, {Name: "MONDOO_PROCFS", Value: "on"}, {Name: "MONDOO_AUTO_UPDATE", Value: "false"}, {Name: "NODE_NAME", Value: node.Name}, {Name: "GOMEMLIMIT", Value: gcLimit}, - }, m.Spec.Nodes.Env), + }, proxyEnvVars...), m.Spec.Nodes.Env), TerminationMessagePath: "/dev/termination-log", TerminationMessagePolicy: corev1.TerminationMessageReadFile, ImagePullPolicy: corev1.PullIfNotPresent, @@ -166,6 +174,15 @@ func CronJob(image string, node corev1.Node, m *v1alpha2.MondooAuditConfig, isOp }, }, } + + // Add imagePullSecrets from MondooOperatorConfig + if len(cfg.Spec.ImagePullSecrets) > 0 { + cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets = append( + cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) + } + + return cj } // DaemonSet creates a DaemonSet for node scanning @@ -177,14 +194,22 @@ func DaemonSet(m v1alpha2.MondooAuditConfig, isOpenshift bool, image string, cfg "--inventory-template", "/etc/opt/mondoo/inventory_template.yml", "--timer", fmt.Sprintf("%d", m.Spec.Nodes.IntervalTimer), } - if cfg.Spec.HttpProxy != nil { - cmd = append(cmd, []string{"--api-proxy", *cfg.Spec.HttpProxy}...) + // Add API proxy if configured (respect SkipProxyForCnspec since node scanning uses cnspec) + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } + } + + var proxyEnvVars []corev1.EnvVar + if !cfg.Spec.SkipProxyForCnspec { + proxyEnvVars = k8s.ProxyEnvVars(cfg) } containerResources := k8s.ResourcesRequirementsWithDefaults(m.Spec.Nodes.Resources, k8s.DefaultNodeScanningResources) gcLimit := gomemlimit.CalculateGoMemLimit(containerResources) - return &appsv1.DaemonSet{ + ds := &appsv1.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: DaemonSetName(m.Name), Namespace: m.Namespace, @@ -234,13 +259,13 @@ func DaemonSet(m v1alpha2.MondooAuditConfig, isOpenshift bool, image string, cfg {Name: "config", ReadOnly: true, MountPath: "/etc/opt/"}, {Name: "temp", MountPath: "/tmp"}, }, - Env: k8s.MergeEnv([]corev1.EnvVar{ + Env: k8s.MergeEnv(append([]corev1.EnvVar{ {Name: "DEBUG", Value: "false"}, {Name: "MONDOO_PROCFS", Value: "on"}, {Name: "MONDOO_AUTO_UPDATE", Value: "false"}, {Name: "GOMEMLIMIT", Value: gcLimit}, {Name: "NODE_NAME", ValueFrom: &corev1.EnvVarSource{FieldRef: &corev1.ObjectFieldSelector{FieldPath: "spec.nodeName"}}}, - }, m.Spec.Nodes.Env), + }, proxyEnvVars...), m.Spec.Nodes.Env), }, }, Volumes: []corev1.Volume{ @@ -281,6 +306,15 @@ func DaemonSet(m v1alpha2.MondooAuditConfig, isOpenshift bool, image string, cfg }, }, } + + // Add imagePullSecrets from MondooOperatorConfig + if len(cfg.Spec.ImagePullSecrets) > 0 { + ds.Spec.Template.Spec.ImagePullSecrets = append( + ds.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) + } + + return ds } // ConfigMap creates a ConfigMap for node scanning inventory diff --git a/controllers/nodes/resources_test.go b/controllers/nodes/resources_test.go index b2953a6bc..f11f7bf97 100644 --- a/controllers/nodes/resources_test.go +++ b/controllers/nodes/resources_test.go @@ -6,6 +6,7 @@ package nodes import ( "crypto/sha256" "fmt" + "strings" "testing" "github.com/stretchr/testify/assert" @@ -20,6 +21,7 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" ) const ( @@ -263,6 +265,132 @@ func TestInventory_WithAnnotations(t *testing.T) { } } +func TestCronJob_WithProxy(t *testing.T) { + testNode := corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "test-node-name"}} + mac := testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + cj := CronJob("test123", testNode, mac, false, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy") + assert.Contains(t, cmdStr, "https://proxy:8443") + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) +} + +func TestCronJob_SkipProxyForCnspec(t *testing.T) { + testNode := corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "test-node-name"}} + mac := testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + SkipProxyForCnspec: true, + }, + } + + cj := CronJob("test123", testNode, mac, false, cfg) + container := cj.Spec.JobTemplate.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + assert.False(t, hasHTTPProxy, "HTTP_PROXY should not be set when SkipProxyForCnspec is true") +} + +func TestCronJob_WithImagePullSecrets(t *testing.T) { + testNode := corev1.Node{ObjectMeta: metav1.ObjectMeta{Name: "test-node-name"}} + mac := testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + cj := CronJob("test123", testNode, mac, false, cfg) + secrets := cj.Spec.JobTemplate.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +func TestDaemonSet_WithProxy(t *testing.T) { + mac := *testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + ds := DaemonSet(mac, false, "test123", cfg, nil) + container := ds.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy") + assert.Contains(t, cmdStr, "https://proxy:8443") + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) +} + +func TestDaemonSet_SkipProxyForCnspec(t *testing.T) { + mac := *testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + SkipProxyForCnspec: true, + }, + } + + ds := DaemonSet(mac, false, "test123", cfg, nil) + container := ds.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + assert.False(t, hasHTTPProxy, "HTTP_PROXY should not be set when SkipProxyForCnspec is true") +} + +func TestDaemonSet_WithImagePullSecrets(t *testing.T) { + mac := *testMondooAuditConfig() + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + ds := DaemonSet(mac, false, "test123", cfg, nil) + secrets := ds.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +// envToMap converts a slice of EnvVar to a map for easy lookup. +func envToMap(envVars []corev1.EnvVar) map[string]string { + m := make(map[string]string, len(envVars)) + for _, e := range envVars { + m[e.Name] = e.Value + } + return m +} + func testMondooAuditConfig() *v1alpha2.MondooAuditConfig { return &v1alpha2.MondooAuditConfig{ ObjectMeta: metav1.ObjectMeta{ diff --git a/controllers/resource_watcher/resources.go b/controllers/resource_watcher/resources.go index f5204d646..618527f2e 100644 --- a/controllers/resource_watcher/resources.go +++ b/controllers/resource_watcher/resources.go @@ -91,9 +91,11 @@ func Deployment(image, integrationMRN, clusterUID string, m *v1alpha2.MondooAudi cmd = append(cmd, "--namespaces-exclude", strings.Join(m.Spec.Filtering.Namespaces.Exclude, ",")) } - // Add API proxy if configured - if cfg.Spec.HttpProxy != nil { - cmd = append(cmd, "--api-proxy", *cfg.Spec.HttpProxy) + // Add API proxy if configured (respect SkipProxyForCnspec since resource watcher uses cnspec) + if !cfg.Spec.SkipProxyForCnspec { + if apiProxy := k8s.APIProxyURL(cfg); apiProxy != nil { + cmd = append(cmd, "--api-proxy", *apiProxy) + } } // Add annotations (sorted for deterministic ordering) @@ -102,6 +104,11 @@ func Deployment(image, integrationMRN, clusterUID string, m *v1alpha2.MondooAudi envVars := feature_flags.AllFeatureFlagsAsEnv() envVars = append(envVars, corev1.EnvVar{Name: "MONDOO_AUTO_UPDATE", Value: "false"}) + // Add proxy environment variables if not skipped for cnspec components + if !cfg.Spec.SkipProxyForCnspec { + envVars = append(envVars, k8s.ProxyEnvVars(cfg)...) + } + // Add custom scanner env vars envVars = append(envVars, m.Spec.Scanner.Env...) @@ -188,5 +195,12 @@ func Deployment(image, integrationMRN, clusterUID string, m *v1alpha2.MondooAudi }, } + // Add imagePullSecrets from MondooOperatorConfig + if len(cfg.Spec.ImagePullSecrets) > 0 { + deployment.Spec.Template.Spec.ImagePullSecrets = append( + deployment.Spec.Template.Spec.ImagePullSecrets, + cfg.Spec.ImagePullSecrets...) + } + return deployment } diff --git a/controllers/resource_watcher/resources_test.go b/controllers/resource_watcher/resources_test.go index c3cc95679..d0aee3275 100644 --- a/controllers/resource_watcher/resources_test.go +++ b/controllers/resource_watcher/resources_test.go @@ -4,11 +4,15 @@ package resource_watcher import ( + "strings" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/utils/ptr" "go.mondoo.com/mondoo-operator/api/v1alpha2" ) @@ -362,3 +366,146 @@ func TestDeployment_EmptyClusterUIDAndIntegrationMRN(t *testing.T) { assert.NotContains(t, cmdStr, "--cluster-uid") assert.NotContains(t, cmdStr, "--integration-mrn") } + +func TestDeployment_HttpsProxyPreferred(t *testing.T) { + config := &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-config", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + KubernetesResources: v1alpha2.KubernetesResources{ + Enable: true, + ResourceWatcher: v1alpha2.ResourceWatcherSpec{ + Enable: true, + }, + }, + }, + } + + operatorConfig := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + deployment := Deployment("ghcr.io/mondoohq/cnspec:latest", "", "", config, operatorConfig) + container := deployment.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.Contains(t, cmdStr, "--api-proxy https://proxy:8443") + assert.NotContains(t, cmdStr, "http://proxy:8080") +} + +func TestDeployment_SkipProxyForCnspec(t *testing.T) { + config := &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-config", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + KubernetesResources: v1alpha2.KubernetesResources{ + Enable: true, + ResourceWatcher: v1alpha2.ResourceWatcherSpec{ + Enable: true, + }, + }, + }, + } + + operatorConfig := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + SkipProxyForCnspec: true, + }, + } + + deployment := Deployment("ghcr.io/mondoohq/cnspec:latest", "", "", config, operatorConfig) + container := deployment.Spec.Template.Spec.Containers[0] + + cmdStr := strings.Join(container.Command, " ") + assert.NotContains(t, cmdStr, "--api-proxy") + + envMap := envToMap(container.Env) + _, hasHTTPProxy := envMap["HTTP_PROXY"] + _, hasHTTPSProxy := envMap["HTTPS_PROXY"] + assert.False(t, hasHTTPProxy, "HTTP_PROXY should not be set when SkipProxyForCnspec is true") + assert.False(t, hasHTTPSProxy, "HTTPS_PROXY should not be set when SkipProxyForCnspec is true") +} + +func TestDeployment_ProxyEnvVars(t *testing.T) { + config := &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-config", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + KubernetesResources: v1alpha2.KubernetesResources{ + Enable: true, + ResourceWatcher: v1alpha2.ResourceWatcherSpec{ + Enable: true, + }, + }, + }, + } + + operatorConfig := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + NoProxy: ptr.To("localhost,10.0.0.0/8"), + }, + } + + deployment := Deployment("ghcr.io/mondoohq/cnspec:latest", "", "", config, operatorConfig) + container := deployment.Spec.Template.Spec.Containers[0] + + envMap := envToMap(container.Env) + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "http://proxy:8080", envMap["http_proxy"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["https_proxy"]) + assert.Equal(t, "localhost,10.0.0.0/8", envMap["NO_PROXY"]) + assert.Equal(t, "localhost,10.0.0.0/8", envMap["no_proxy"]) +} + +func TestDeployment_WithImagePullSecrets(t *testing.T) { + config := &v1alpha2.MondooAuditConfig{ + ObjectMeta: metav1.ObjectMeta{ + Name: "my-config", + Namespace: "mondoo-operator", + }, + Spec: v1alpha2.MondooAuditConfigSpec{ + KubernetesResources: v1alpha2.KubernetesResources{ + Enable: true, + ResourceWatcher: v1alpha2.ResourceWatcherSpec{ + Enable: true, + }, + }, + }, + } + + operatorConfig := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + ImagePullSecrets: []corev1.LocalObjectReference{ + {Name: "my-registry-secret"}, + }, + }, + } + + deployment := Deployment("ghcr.io/mondoohq/cnspec:latest", "", "", config, operatorConfig) + secrets := deployment.Spec.Template.Spec.ImagePullSecrets + require.Len(t, secrets, 1) + assert.Equal(t, "my-registry-secret", secrets[0].Name) +} + +// envToMap converts a slice of EnvVar to a map for easy lookup. +func envToMap(envVars []corev1.EnvVar) map[string]string { + m := make(map[string]string, len(envVars)) + for _, e := range envVars { + m[e.Name] = e.Value + } + return m +} diff --git a/controllers/status/status_reporter.go b/controllers/status/status_reporter.go index 73fa3d901..835d40e98 100644 --- a/controllers/status/status_reporter.go +++ b/controllers/status/status_reporter.go @@ -82,6 +82,8 @@ func (r *StatusReporter) Report(ctx context.Context, m v1alpha2.MondooAuditConfi ApiEndpoint: serviceAccount.ApiEndpoint, Token: token, HttpProxy: cfg.Spec.HttpProxy, + HttpsProxy: cfg.Spec.HttpsProxy, + NoProxy: cfg.Spec.NoProxy, }) if err != nil { return err diff --git a/docs/operator-config.md b/docs/operator-config.md new file mode 100644 index 000000000..e4ec117f4 --- /dev/null +++ b/docs/operator-config.md @@ -0,0 +1,508 @@ +# MondooOperatorConfig Guide + +This guide explains how to configure the Mondoo Operator for enterprise environments including corporate proxies, air-gapped clusters, and private container registries. + +- [MondooOperatorConfig Guide](#mondoooperatorconfig-guide) + - [Overview](#overview) + - [Quick Start](#quick-start) + - [Using Helm](#using-helm) + - [Using kubectl](#using-kubectl) + - [Configuration Reference](#configuration-reference) + - [Use Cases](#use-cases) + - [Corporate Proxy Configuration](#corporate-proxy-configuration) + - [Air-Gapped / Disconnected Clusters](#air-gapped--disconnected-clusters) + - [Private Registry Authentication](#private-registry-authentication) + - [GKE Autopilot / Restricted Environments](#gke-autopilot--restricted-environments) + - [Metrics and Monitoring](#metrics-and-monitoring) + - [How Configuration Flows to Components](#how-configuration-flows-to-components) + - [Troubleshooting](#troubleshooting) + - [Helm Configuration Reference](#helm-configuration-reference) + +## Overview + +`MondooOperatorConfig` is a **cluster-scoped** custom resource that configures operator-wide settings. Unlike `MondooAuditConfig` (which defines what to scan), `MondooOperatorConfig` defines how the operator itself behaves across all scanning workloads. + +Key characteristics: + +- **Cluster-scoped**: One instance applies to the entire cluster +- **Fixed name**: Must be named `mondoo-operator-config` +- **Single instance**: Only one `MondooOperatorConfig` is allowed per cluster +- **Applies globally**: Settings affect all `MondooAuditConfig` resources + +**Relationship to MondooAuditConfig:** + +| Resource | Scope | Purpose | +|----------|-------|---------| +| `MondooOperatorConfig` | Cluster | Operator-wide settings (proxies, registries, metrics) | +| `MondooAuditConfig` | Namespace | What to scan (nodes, K8s resources, containers) | + +## Quick Start + +### Using Helm + +The simplest way to configure `MondooOperatorConfig` is through Helm values: + +```bash +helm install mondoo-operator mondoo/mondoo-operator \ + --namespace mondoo-operator \ + --create-namespace \ + --set operator.httpProxy="http://proxy.example.com:3128" \ + --set operator.httpsProxy="http://proxy.example.com:3128" +``` + +### Using kubectl + +Apply a `MondooOperatorConfig` resource directly: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + metrics: + enable: true +``` + +```bash +kubectl apply -f mondoo-operator-config.yaml +``` + +## Configuration Reference + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `metrics.enable` | bool | `false` | Enable Prometheus metrics and ServiceMonitor creation | +| `metrics.resourceLabels` | map[string]string | `{}` | Extra labels to add to metrics-related resources (e.g., ServiceMonitor) | +| `httpProxy` | string | `""` | HTTP proxy URL for outbound connections | +| `httpsProxy` | string | `""` | HTTPS proxy URL for outbound connections | +| `noProxy` | string | `""` | Comma-separated list of hosts/CIDRs that bypass the proxy | +| `containerProxy` | string | `""` | Proxy for container image operations | +| `imagePullSecrets` | []LocalObjectReference | `[]` | Secrets for pulling Mondoo container images | +| `imageRegistry` | string | `""` | Custom registry prefix for all Mondoo images (simple mirror) | +| `registryMirrors` | map[string]string | `{}` | Map of public registries to private mirrors | +| `skipContainerResolution` | bool | `false` | Skip resolving container image digests from upstream | +| `skipProxyForCnspec` | bool | `false` | Disable proxy settings for cnspec-based components | + +## Use Cases + +### Corporate Proxy Configuration + +Configure the operator to route traffic through your corporate proxy: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + httpProxy: "http://proxy.example.com:3128" + httpsProxy: "http://proxy.example.com:3128" + noProxy: "10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,.cluster.local,.svc,localhost,127.0.0.1" +``` + +**noProxy format:** + +The `noProxy` field accepts a comma-separated list following the same conventions as the standard `NO_PROXY` environment variable: + +| Pattern | Description | Example | +|---------|-------------|---------| +| IP address | Exact IP match | `192.168.1.1` | +| CIDR notation | IP range | `10.0.0.0/8` | +| Domain | Exact domain match | `internal.example.com` | +| Domain suffix | Matches domain and subdomains (leading `.`) | `.example.com` | +| Wildcard | Matches all hosts (use carefully) | `*` | + +**Recommended noProxy entries for Kubernetes:** + +``` +10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,.cluster.local,.svc,localhost,127.0.0.1 +``` + +This bypasses proxy for: +- Private IP ranges (pod and service CIDRs) +- Kubernetes DNS domains +- Localhost connections + +### Air-Gapped / Disconnected Clusters + +For clusters without internet access, configure the operator to use your internal registry: + +**Option 1: Simple registry mirror (all images from one mirror)** + +Use `imageRegistry` when all Mondoo images are mirrored to the same registry: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + # Rewrites: ghcr.io/mondoohq/cnspec:latest -> registry.example.com/ghcr.io.docker/mondoohq/cnspec:latest + imageRegistry: "registry.example.com/ghcr.io.docker" + # Skip attempts to resolve latest image digests from upstream + skipContainerResolution: true + # Image pull credentials + imagePullSecrets: + - name: registry-credentials +``` + +**Option 2: Multiple registry mirrors (different mirrors per source)** + +Use `registryMirrors` when different source registries map to different mirrors: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + registryMirrors: + ghcr.io: "artifactory.example.com/ghcr-remote" + docker.io: "artifactory.example.com/docker-hub-remote" + quay.io: "artifactory.example.com/quay-remote" + skipContainerResolution: true + imagePullSecrets: + - name: artifactory-credentials +``` + +> **Note:** If both `imageRegistry` and `registryMirrors` are set, `registryMirrors` takes precedence. + +**Step-by-step air-gapped setup:** + +1. **Mirror the required images** to your internal registry: + - `ghcr.io/mondoohq/mondoo-operator:` + - `ghcr.io/mondoohq/cnspec:` + +2. **Create the image pull secret:** + ```bash + kubectl create secret docker-registry registry-credentials \ + --namespace mondoo-operator \ + --docker-server=registry.example.com \ + --docker-username=user \ + --docker-password=password + ``` + +3. **Apply the MondooOperatorConfig** with your registry settings + +4. **Deploy MondooAuditConfig** as normal - the operator applies registry settings automatically + +### Private Registry Authentication + +Configure credentials for pulling Mondoo images from authenticated registries: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + imagePullSecrets: + - name: ghcr-credentials + - name: docker-hub-credentials +``` + +**Creating the required secrets:** + +```bash +# For GitHub Container Registry +kubectl create secret docker-registry ghcr-credentials \ + --namespace mondoo-operator \ + --docker-server=ghcr.io \ + --docker-username=USERNAME \ + --docker-password=GITHUB_TOKEN + +# For Docker Hub (rate limiting) +kubectl create secret docker-registry docker-hub-credentials \ + --namespace mondoo-operator \ + --docker-server=docker.io \ + --docker-username=USERNAME \ + --docker-password=PASSWORD +``` + +**Which components use imagePullSecrets:** + +The `imagePullSecrets` from `MondooOperatorConfig` are applied to all Mondoo-managed workloads: +- Node scanning DaemonSets +- Kubernetes resource scanning CronJobs +- Container image scanning CronJobs + +> **Note:** These are different from `scanner.privateRegistriesPullSecretRef` in `MondooAuditConfig`, which provides credentials for scanning your application's container images (not for pulling Mondoo's own images). + +### GKE Autopilot / Restricted Environments + +Some managed Kubernetes environments restrict certain operations. Use these settings to work around limitations: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + # Skip image digest resolution (requires outbound HTTPS to ghcr.io) + skipContainerResolution: true + # Don't set proxy env vars for cnspec (useful when API is internal) + skipProxyForCnspec: true +``` + +**When to use `skipContainerResolution`:** + +- Air-gapped clusters without access to ghcr.io +- GKE Autopilot where network policies block registry access +- Environments where you want to pin exact image versions + +**When to use `skipProxyForCnspec`:** + +- Your Mondoo API endpoint is internal (no proxy needed) +- Proxy causes issues with certificate validation +- cnspec components need direct access but other components need proxy + +### Metrics and Monitoring + +Enable Prometheus metrics collection: + +```yaml +apiVersion: k8s.mondoo.com/v1alpha2 +kind: MondooOperatorConfig +metadata: + name: mondoo-operator-config +spec: + metrics: + enable: true + resourceLabels: + prometheus: main + team: security +``` + +**What happens when metrics are enabled:** + +1. The operator creates a `ServiceMonitor` resource for Prometheus Operator +2. Metrics are exposed on the operator's metrics endpoint (port 8080) +3. The `resourceLabels` are added to the ServiceMonitor for label-based selection + +**Prometheus Operator integration:** + +If you use Prometheus Operator, ensure your Prometheus instance is configured to select the ServiceMonitor. The `resourceLabels` can be used to match your Prometheus's `serviceMonitorSelector`: + +```yaml +# Example Prometheus configuration +apiVersion: monitoring.coreos.com/v1 +kind: Prometheus +metadata: + name: main +spec: + serviceMonitorSelector: + matchLabels: + prometheus: main # Matches the resourceLabel above +``` + +**Available metrics:** + +The operator exports standard controller-runtime metrics including: +- Reconciliation counts and durations +- Work queue depth and latency +- Controller error counts + +## How Configuration Flows to Components + +The following table shows which `MondooOperatorConfig` settings affect which components: + +| Setting | Operator | Node Scanning | K8s Resource Scanning | Container Scanning | +|---------|:--------:|:-------------:|:---------------------:|:------------------:| +| `httpProxy` | | ✓ | ✓ | ✓* | +| `httpsProxy` | | ✓ | ✓ | ✓* | +| `noProxy` | | ✓ | ✓ | ✓* | +| `containerProxy` | | | | ✓ | +| `imagePullSecrets` | | ✓ | ✓ | ✓ | +| `imageRegistry` | ✓ | ✓ | ✓ | ✓ | +| `registryMirrors` | ✓ | ✓ | ✓ | ✓ | +| `skipContainerResolution` | ✓ | | | | +| `skipProxyForCnspec` | | | ✓ | ✓ | +| `metrics.enable` | ✓ | | | | + +*\* Unless `skipProxyForCnspec: true`* + +**Configuration inheritance:** + +``` +MondooOperatorConfig (cluster-wide settings) + │ + ▼ + ┌──────────────┐ + │ Operator │ ◄── Uses imageRegistry, skipContainerResolution, metrics + └──────────────┘ + │ + │ Creates workloads with inherited settings + ▼ + ┌──────────────────────────────────────────────────┐ + │ MondooAuditConfig │ + │ ┌────────────┐ ┌────────────┐ ┌──────────────┐ │ + │ │ Nodes │ │ K8s Scan │ │ Containers │ │ + │ │ DaemonSet │ │ CronJob │ │ CronJob │ │ + │ └────────────┘ └────────────┘ └──────────────┘ │ + │ │ │ │ │ + │ └──────────────┼───────────────┘ │ + │ ▼ │ + │ Proxy, ImagePullSecrets, Registry │ + └──────────────────────────────────────────────────┘ +``` + +## Troubleshooting + +### Images not pulling from private registry + +**Symptoms:** Pods fail with `ImagePullBackOff` or `ErrImagePull` + +**Checklist:** + +1. Verify the secret exists in the `mondoo-operator` namespace: + ```bash + kubectl get secrets -n mondoo-operator + ``` + +2. Verify the secret is correctly formatted: + ```bash + kubectl get secret registry-credentials -n mondoo-operator -o jsonpath='{.data.\.dockerconfigjson}' | base64 -d + ``` + +3. Test the credentials manually: + ```bash + docker login registry.example.com -u user -p password + docker pull registry.example.com/ghcr.io.docker/mondoohq/cnspec:latest + ``` + +4. Check that `imagePullSecrets` references the correct secret name in `MondooOperatorConfig` + +5. Restart the operator to pick up changes: + ```bash + kubectl rollout restart deployment mondoo-operator-controller-manager -n mondoo-operator + ``` + +### Proxy not working for some components + +**Symptoms:** Some scans work, others fail with connection errors + +**Checklist:** + +1. Verify proxy environment variables in the pod: + ```bash + kubectl exec -n mondoo-operator -- env | grep -i proxy + ``` + +2. Check if `skipProxyForCnspec` is accidentally enabled: + ```bash + kubectl get mondoooperatorconfig mondoo-operator-config -o yaml | grep skipProxyForCnspec + ``` + +3. Verify `noProxy` includes Kubernetes internal addresses: + ``` + noProxy: "10.0.0.0/8,.cluster.local,.svc,localhost" + ``` + +4. Test proxy connectivity from within a pod: + ```bash + kubectl run -it --rm proxy-test --image=curlimages/curl -- \ + curl -x http://proxy.example.com:3128 https://api.mondoo.com + ``` + +### MondooOperatorConfig not being applied + +**Symptoms:** Configuration changes don't take effect + +**Checklist:** + +1. Verify the resource name is exactly `mondoo-operator-config`: + ```bash + kubectl get mondoooperatorconfig + ``` + +2. Check for validation errors in the resource: + ```bash + kubectl describe mondoooperatorconfig mondoo-operator-config + ``` + +3. Check operator logs for configuration loading: + ```bash + kubectl logs -n mondoo-operator deployment/mondoo-operator-controller-manager | grep -i config + ``` + +4. Restart the operator to force configuration reload: + ```bash + kubectl rollout restart deployment mondoo-operator-controller-manager -n mondoo-operator + ``` + +### Metrics not appearing in Prometheus + +**Symptoms:** ServiceMonitor created but no metrics in Prometheus + +**Checklist:** + +1. Verify ServiceMonitor exists: + ```bash + kubectl get servicemonitor -n mondoo-operator + ``` + +2. Check ServiceMonitor labels match your Prometheus selector: + ```bash + kubectl get servicemonitor -n mondoo-operator -o yaml | grep -A5 labels + ``` + +3. Verify Prometheus is configured to watch the namespace: + ```bash + kubectl get prometheus -o yaml | grep -A10 serviceMonitorNamespaceSelector + ``` + +4. Check Prometheus targets page for the mondoo-operator target + +5. Verify the metrics endpoint is accessible: + ```bash + kubectl port-forward -n mondoo-operator svc/mondoo-operator-controller-manager-metrics-service 8080:8080 + curl http://localhost:8080/metrics + ``` + +## Helm Configuration Reference + +When using Helm, the following `values.yaml` settings map to `MondooOperatorConfig` fields: + +```yaml +operator: + # Create MondooOperatorConfig resource + createConfig: true + + # Proxy settings + httpProxy: "http://proxy.example.com:3128" + httpsProxy: "http://proxy.example.com:3128" + noProxy: "10.0.0.0/8,.cluster.local" + containerProxy: "" + + # Registry settings + imageRegistry: "registry.example.com/ghcr.io.docker" + registryMirrors: + ghcr.io: "registry.example.com/ghcr-remote" + docker.io: "registry.example.com/docker-hub-remote" + + # Image pull credentials + imagePullSecrets: + - name: registry-credentials + + # Behavior flags + skipContainerResolution: true + skipProxyForCnspec: false +``` + +**Mapping table:** + +| Helm Value | MondooOperatorConfig Field | +|------------|---------------------------| +| `operator.createConfig` | Controls whether CR is created | +| `operator.httpProxy` | `spec.httpProxy` | +| `operator.httpsProxy` | `spec.httpsProxy` | +| `operator.noProxy` | `spec.noProxy` | +| `operator.containerProxy` | `spec.containerProxy` | +| `operator.imageRegistry` | `spec.imageRegistry` | +| `operator.registryMirrors` | `spec.registryMirrors` | +| `operator.imagePullSecrets` | `spec.imagePullSecrets` | +| `operator.skipContainerResolution` | `spec.skipContainerResolution` | +| `operator.skipProxyForCnspec` | `spec.skipProxyForCnspec` | + +> **Note:** Metrics configuration is not currently exposed via Helm values. Apply a `MondooOperatorConfig` directly to enable metrics. diff --git a/docs/user-manual.md b/docs/user-manual.md index 995518270..8924f8682 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -3,6 +3,7 @@ This user manual describes how to install and use the Mondoo Operator. - [User manual](#user-manual) + - [Configuring the Operator](#configuring-the-operator) - [Mondoo Operator Installation](#mondoo-operator-installation) - [Installing with kubectl](#installing-with-kubectl) - [Installing with Helm](#installing-with-helm) @@ -51,6 +52,11 @@ When upgrading from operator versions prior to v12.x, the following changes appl - **High-priority resources by default**: The watcher now only monitors Deployments, DaemonSets, StatefulSets, and ReplicaSets by default (previously watched all resources including Pods and Jobs). To restore the previous behavior, set `watchAllResources: true` in your configuration. +## Configuring the Operator + +For cluster-wide settings like proxies, registry mirrors, and metrics, see the +[MondooOperatorConfig Guide](operator-config.md). + ## Mondoo Operator Installation Install the Mondoo Operator using kubectl, Helm, or Operator Lifecycle Manager. diff --git a/hack/update-helm-crds.sh b/hack/update-helm-crds.sh index 65c3b8680..782b8b538 100755 --- a/hack/update-helm-crds.sh +++ b/hack/update-helm-crds.sh @@ -2,10 +2,9 @@ # Copyright Mondoo, Inc. 2026 # SPDX-License-Identifier: BUSL-1.1 # -# This script updates the CRD templates in the Helm chart from the generated CRDs. -# It applies the necessary transformations to make them Helm-compatible: -# - Adds Helm labels template -# - Replaces webhook namespace with Helm template +# This script updates the CRDs in the Helm chart from the generated CRDs. +# CRDs are placed in charts/mondoo-operator/crds/ which Helm installs +# automatically before other chart resources. # # Usage: ./hack/update-helm-crds.sh @@ -13,58 +12,13 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" -CHART_DIR="${ROOT_DIR}/charts/mondoo-operator/templates" +CRD_BASES="${ROOT_DIR}/config/crd/bases" +CHART_CRDS="${ROOT_DIR}/charts/mondoo-operator/crds" -# Check for required tools -if ! command -v yq &> /dev/null; then - echo "Error: yq is required but not installed." - echo "Install with: brew install yq (macOS) or go install github.com/mikefarah/yq/v4@latest" - exit 1 -fi +echo "Copying CRDs from ${CRD_BASES} to ${CHART_CRDS}..." +cp "${CRD_BASES}/k8s.mondoo.com_mondooauditconfigs.yaml" "${CHART_CRDS}/" +cp "${CRD_BASES}/k8s.mondoo.com_mondoooperatorconfigs.yaml" "${CHART_CRDS}/" -if ! command -v kustomize &> /dev/null && [ ! -f "${ROOT_DIR}/bin/kustomize" ]; then - echo "Error: kustomize is required. Run 'make kustomize' first." - exit 1 -fi - -KUSTOMIZE="${ROOT_DIR}/bin/kustomize" -if [ ! -f "$KUSTOMIZE" ]; then - KUSTOMIZE="kustomize" -fi - -echo "Building CRDs with kustomize..." -CRD_OUTPUT=$("$KUSTOMIZE" build "${ROOT_DIR}/config/crd") - -# Process mondooauditconfigs CRD -echo "Processing mondooauditconfigs CRD..." -echo "$CRD_OUTPUT" | yq eval 'select(.metadata.name == "mondooauditconfigs.k8s.mondoo.com")' - | \ - yq eval 'del(.metadata.labels)' - | \ - yq eval '.spec.conversion.webhook.clientConfig.service.namespace = "HELM_NAMESPACE_PLACEHOLDER"' - | \ - sed 's/name: mondooauditconfigs.k8s.mondoo.com/name: mondooauditconfigs.k8s.mondoo.com\n labels:\n {{- include "mondoo-operator.labels" . | nindent 4 }}/' | \ - sed "s/HELM_NAMESPACE_PLACEHOLDER/'{{ .Release.Namespace }}'/" \ - > "${CHART_DIR}/mondooauditconfig-crd.yaml" - -# Process mondoooperatorconfigs CRD -echo "Processing mondoooperatorconfigs CRD..." -echo "$CRD_OUTPUT" | yq eval 'select(.metadata.name == "mondoooperatorconfigs.k8s.mondoo.com")' - | \ - yq eval 'del(.metadata.labels)' - | \ - sed 's/name: mondoooperatorconfigs.k8s.mondoo.com/name: mondoooperatorconfigs.k8s.mondoo.com\n labels:\n {{- include "mondoo-operator.labels" . | nindent 4 }}/' \ - > "${CHART_DIR}/mondoooperatorconfig-crd.yaml" - -# Validate that Helm template directives were injected correctly -echo "Validating Helm template directives..." -for crd_file in "${CHART_DIR}/mondooauditconfig-crd.yaml" "${CHART_DIR}/mondoooperatorconfig-crd.yaml"; do - if ! grep -q 'include "mondoo-operator.labels"' "$crd_file"; then - echo "Error: Helm labels template not found in $(basename "$crd_file")" - exit 1 - fi -done - -if ! grep -q '\.Release\.Namespace' "${CHART_DIR}/mondooauditconfig-crd.yaml"; then - echo "Error: Helm namespace template not found in mondooauditconfig-crd.yaml" - exit 1 -fi - -echo "CRDs updated successfully in ${CHART_DIR}" +echo "CRDs updated successfully in ${CHART_CRDS}" echo "" -echo "Please review the changes with: git diff charts/mondoo-operator/templates/*-crd.yaml" +echo "Please review the changes with: git diff charts/mondoo-operator/crds/" diff --git a/pkg/client/common/http.go b/pkg/client/common/http.go index 899a38c10..b301d06e8 100644 --- a/pkg/client/common/http.go +++ b/pkg/client/common/http.go @@ -11,6 +11,7 @@ import ( "net" "net/http" "net/url" + "strings" "time" ) @@ -23,6 +24,10 @@ const ( ) func DefaultHttpClient(httpProxy *string, httpTimeout *time.Duration) (http.Client, error) { + return DefaultHttpClientWithProxy(httpProxy, nil, nil, httpTimeout) +} + +func DefaultHttpClientWithProxy(httpProxy *string, httpsProxy *string, noProxy *string, httpTimeout *time.Duration) (http.Client, error) { tr := &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ @@ -35,12 +40,34 @@ func DefaultHttpClient(httpProxy *string, httpTimeout *time.Duration) (http.Clie ExpectContinueTimeout: 1 * time.Second, } - if httpProxy != nil { - urlParsed, err := url.Parse(*httpProxy) - if err != nil { - return http.Client{}, err + if httpProxy != nil || httpsProxy != nil { + var httpProxyURL, httpsProxyURL *url.URL + var err error + if httpProxy != nil { + httpProxyURL, err = url.Parse(*httpProxy) + if err != nil { + return http.Client{}, fmt.Errorf("failed to parse httpProxy: %w", err) + } + } + if httpsProxy != nil { + httpsProxyURL, err = url.Parse(*httpsProxy) + if err != nil { + return http.Client{}, fmt.Errorf("failed to parse httpsProxy: %w", err) + } + } + // Create a proxy function that respects noProxy and uses the correct proxy per scheme + tr.Proxy = func(req *http.Request) (*url.URL, error) { + if noProxy != nil && shouldBypassProxy(req.URL.Host, *noProxy) { + return nil, nil // No proxy for this host + } + if req.URL.Scheme == "https" && httpsProxyURL != nil { + return httpsProxyURL, nil + } + if httpProxyURL != nil { + return httpProxyURL, nil + } + return nil, nil } - tr.Proxy = http.ProxyURL(urlParsed) } timeout := defaultHttpTimeout if httpTimeout != nil { @@ -52,6 +79,66 @@ func DefaultHttpClient(httpProxy *string, httpTimeout *time.Duration) (http.Clie }, nil } +// shouldBypassProxy checks if the given host should bypass the proxy based on noProxy settings. +// Matching is case-insensitive since DNS names are case-insensitive. +func shouldBypassProxy(host string, noProxy string) bool { + if noProxy == "" { + return false + } + + // Remove port from host if present (handles IPv6 correctly) + hostWithoutPort := host + if h, _, err := net.SplitHostPort(host); err == nil { + hostWithoutPort = h + } + + // Lowercase for case-insensitive comparison (DNS is case-insensitive) + hostLower := strings.ToLower(hostWithoutPort) + + // Check each entry in the noProxy list + for _, entry := range strings.Split(noProxy, ",") { + entry = strings.TrimSpace(entry) + if entry == "" { + continue + } + + // Handle wildcard "*" - bypass all + if entry == "*" { + return true + } + + // Lowercase entry for case-insensitive comparison + entryLower := strings.ToLower(entry) + + // Handle domain suffix matching (e.g., ".example.com" matches "api.mondoo.example.com") + if strings.HasPrefix(entryLower, ".") { + if strings.HasSuffix(hostLower, entryLower) || hostLower == entryLower[1:] { + return true + } + continue + } + + // Handle CIDR notation for IP ranges + if strings.Contains(entry, "/") { + _, cidr, err := net.ParseCIDR(entry) + if err == nil { + ip := net.ParseIP(hostWithoutPort) + if ip != nil && cidr.Contains(ip) { + return true + } + } + continue + } + + // Exact match or suffix match + if hostLower == entryLower || strings.HasSuffix(hostLower, "."+entryLower) { + return true + } + } + + return false +} + func Request(ctx context.Context, client http.Client, url, token string, reqBodyBytes []byte) ([]byte, error) { header := make(http.Header) header.Set("Accept", "application/json") diff --git a/pkg/client/common/http_test.go b/pkg/client/common/http_test.go new file mode 100644 index 000000000..f6497370c --- /dev/null +++ b/pkg/client/common/http_test.go @@ -0,0 +1,224 @@ +// Copyright Mondoo, Inc. 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package common + +import ( + "testing" +) + +func TestShouldBypassProxy(t *testing.T) { + tests := []struct { + name string + host string + noProxy string + expected bool + }{ + // Empty noProxy + { + name: "empty noProxy returns false", + host: "api.mondoo.com", + noProxy: "", + expected: false, + }, + // Wildcard "*" + { + name: "wildcard matches any host", + host: "api.mondoo.com", + noProxy: "*", + expected: true, + }, + { + name: "wildcard matches localhost", + host: "localhost", + noProxy: "*", + expected: true, + }, + { + name: "wildcard matches IP address", + host: "10.1.2.3", + noProxy: "*", + expected: true, + }, + // Exact match + { + name: "exact match matches", + host: "api.mondoo.com", + noProxy: "api.mondoo.com", + expected: true, + }, + { + name: "exact match does not match different host", + host: "other.mondoo.com", + noProxy: "api.mondoo.com", + expected: false, + }, + { + name: "exact match is case insensitive", + host: "API.MONDOO.COM", + noProxy: "api.mondoo.com", + expected: true, + }, + // Domain suffix with leading dot + { + name: "domain suffix .example.com matches api.example.com", + host: "api.example.com", + noProxy: ".example.com", + expected: true, + }, + { + name: "domain suffix .example.com matches foo.bar.example.com", + host: "foo.bar.example.com", + noProxy: ".example.com", + expected: true, + }, + { + name: "domain suffix .example.com matches example.com exactly", + host: "example.com", + noProxy: ".example.com", + expected: true, + }, + { + name: "domain suffix .example.com does not match notexample.com", + host: "notexample.com", + noProxy: ".example.com", + expected: false, + }, + // Suffix without leading dot + { + name: "suffix example.com matches api.example.com", + host: "api.example.com", + noProxy: "example.com", + expected: true, + }, + { + name: "suffix example.com matches example.com exactly", + host: "example.com", + noProxy: "example.com", + expected: true, + }, + { + name: "suffix example.com does not match notexample.com", + host: "notexample.com", + noProxy: "example.com", + expected: false, + }, + // CIDR notation + { + name: "CIDR 10.0.0.0/8 matches 10.1.2.3", + host: "10.1.2.3", + noProxy: "10.0.0.0/8", + expected: true, + }, + { + name: "CIDR 10.0.0.0/8 matches 10.255.255.255", + host: "10.255.255.255", + noProxy: "10.0.0.0/8", + expected: true, + }, + { + name: "CIDR 10.0.0.0/8 does not match 192.168.1.1", + host: "192.168.1.1", + noProxy: "10.0.0.0/8", + expected: false, + }, + { + name: "CIDR 192.168.0.0/16 matches 192.168.1.1", + host: "192.168.1.1", + noProxy: "192.168.0.0/16", + expected: true, + }, + { + name: "CIDR does not apply to hostnames", + host: "api.mondoo.com", + noProxy: "10.0.0.0/8", + expected: false, + }, + // Host with port + { + name: "host with port strips port before matching", + host: "api.mondoo.com:443", + noProxy: "api.mondoo.com", + expected: true, + }, + { + name: "host with port matches domain suffix", + host: "api.example.com:8080", + noProxy: ".example.com", + expected: true, + }, + { + name: "IP with port matches CIDR", + host: "10.1.2.3:9000", + noProxy: "10.0.0.0/8", + expected: true, + }, + // Multiple entries + { + name: "multiple entries matches first", + host: "localhost", + noProxy: "localhost,.local,10.0.0.0/8", + expected: true, + }, + { + name: "multiple entries matches second", + host: "myhost.local", + noProxy: "localhost,.local,10.0.0.0/8", + expected: true, + }, + { + name: "multiple entries matches third CIDR", + host: "10.1.2.3", + noProxy: "localhost,.local,10.0.0.0/8", + expected: true, + }, + { + name: "multiple entries does not match any", + host: "api.mondoo.com", + noProxy: "localhost,.local,10.0.0.0/8", + expected: false, + }, + // Whitespace handling + { + name: "whitespace around entries is trimmed", + host: "localhost", + noProxy: " localhost , .local ", + expected: true, + }, + { + name: "whitespace in entries is trimmed for domain", + host: "api.local", + noProxy: " localhost , .local ", + expected: true, + }, + { + name: "empty entries after split are ignored", + host: "localhost", + noProxy: "localhost,,,.local", + expected: true, + }, + // Edge cases + { + name: "single localhost entry", + host: "localhost", + noProxy: "localhost", + expected: true, + }, + { + name: "invalid CIDR is ignored", + host: "10.1.2.3", + noProxy: "invalid/cidr", + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := shouldBypassProxy(tt.host, tt.noProxy) + if result != tt.expected { + t.Errorf("shouldBypassProxy(%q, %q) = %v, expected %v", + tt.host, tt.noProxy, result, tt.expected) + } + }) + } +} diff --git a/pkg/client/mondooclient/client.go b/pkg/client/mondooclient/client.go index b0a3d709f..fbd7cb22a 100644 --- a/pkg/client/mondooclient/client.go +++ b/pkg/client/mondooclient/client.go @@ -26,6 +26,8 @@ type MondooClientOptions struct { ApiEndpoint string Token string HttpProxy *string + HttpsProxy *string + NoProxy *string HttpTimeout *time.Duration } @@ -37,7 +39,7 @@ type mondooClient struct { func NewClient(opts MondooClientOptions) (MondooClient, error) { opts.ApiEndpoint = strings.TrimRight(opts.ApiEndpoint, "/") - client, err := common.DefaultHttpClient(opts.HttpProxy, opts.HttpTimeout) + client, err := common.DefaultHttpClientWithProxy(opts.HttpProxy, opts.HttpsProxy, opts.NoProxy, opts.HttpTimeout) if err != nil { return nil, err } diff --git a/pkg/imagecache/imagecache.go b/pkg/imagecache/imagecache.go index 102fc96ad..4f2a68d33 100644 --- a/pkg/imagecache/imagecache.go +++ b/pkg/imagecache/imagecache.go @@ -4,12 +4,22 @@ package imagecache import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "strings" "sync" "time" + "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "go.mondoo.com/cnquery/v12/providers/os/connection/container/auth" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "sigs.k8s.io/controller-runtime/pkg/client" + ctrl "sigs.k8s.io/controller-runtime/pkg/log" ) const ( @@ -18,12 +28,15 @@ const ( type ImageCacher interface { GetImage(string) (string, error) + // WithAuth returns a new ImageCacher that uses the provided authentication keychain + WithAuth(keychain authn.Keychain) ImageCacher } type imageCache struct { images map[string]imageData - imagesMutex sync.RWMutex + imagesMutex *sync.RWMutex fetchImage func(string) (string, error) + keychain authn.Keychain } type imageData struct { @@ -81,13 +94,20 @@ func (i *imageCache) updateImage(image string) error { return nil } -func queryImageWithSHA(image string) (string, error) { +func queryImageWithSHA(image string, keychain authn.Keychain) (string, error) { ref, err := name.ParseReference(image) if err != nil { return "", err } - kc := auth.ConstructKeychain(ref.Name()) + // Use provided keychain if given, otherwise use cnquery's auth which supports ECR, GCR, etc. + var kc authn.Keychain + if keychain != nil { + kc = keychain + } else { + kc = auth.ConstructKeychain(ref.Name()) + } + desc, err := remote.Get(ref, remote.WithAuthFromKeychain(kc)) if err != nil { return "", err @@ -101,7 +121,129 @@ func queryImageWithSHA(image string) (string, error) { func NewImageCacher() ImageCacher { return &imageCache{ - images: map[string]imageData{}, - fetchImage: queryImageWithSHA, + images: map[string]imageData{}, + imagesMutex: &sync.RWMutex{}, + fetchImage: func(image string) (string, error) { + return queryImageWithSHA(image, nil) + }, + } +} + +func (i *imageCache) WithAuth(keychain authn.Keychain) ImageCacher { + return &imageCache{ + images: i.images, + imagesMutex: i.imagesMutex, + fetchImage: func(image string) (string, error) { + return queryImageWithSHA(image, keychain) + }, + keychain: keychain, + } +} + +// DockerConfigJSON represents the structure of a Docker config.json file +type DockerConfigJSON struct { + Auths map[string]DockerConfigEntry `json:"auths"` +} + +// DockerConfigEntry represents a single registry entry in Docker config +type DockerConfigEntry struct { + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` + Auth string `json:"auth,omitempty"` +} + +// KeychainFromSecrets creates an authn.Keychain from Kubernetes imagePullSecrets +func KeychainFromSecrets(ctx context.Context, kubeClient client.Client, namespace string, secretRefs []corev1.LocalObjectReference) (authn.Keychain, error) { + log := ctrl.Log.WithName("imagecache") + + if len(secretRefs) == 0 { + return authn.DefaultKeychain, nil } + + var configs []DockerConfigJSON + for _, secretRef := range secretRefs { + secret := &corev1.Secret{} + if err := kubeClient.Get(ctx, client.ObjectKey{Namespace: namespace, Name: secretRef.Name}, secret); err != nil { + if errors.IsNotFound(err) { + log.Info("imagePullSecret not found, skipping", "secret", secretRef.Name, "namespace", namespace) + continue + } + return nil, fmt.Errorf("failed to get imagePullSecret %s/%s: %w", namespace, secretRef.Name, err) + } + + // Handle both .dockerconfigjson and .dockercfg formats + var configData []byte + if data, ok := secret.Data[".dockerconfigjson"]; ok { + configData = data + } else if data, ok := secret.Data[".dockercfg"]; ok { + configData = data + } else { + log.Info("imagePullSecret has no .dockerconfigjson or .dockercfg data, skipping", "secret", secretRef.Name, "namespace", namespace) + continue + } + + var config DockerConfigJSON + if err := json.Unmarshal(configData, &config); err != nil { + log.Error(err, "failed to parse imagePullSecret data", "secret", secretRef.Name, "namespace", namespace) + continue + } + configs = append(configs, config) + } + + return &multiKeychain{configs: configs}, nil +} + +// multiKeychain implements authn.Keychain for multiple Docker configs +type multiKeychain struct { + configs []DockerConfigJSON +} + +func (k *multiKeychain) Resolve(resource authn.Resource) (authn.Authenticator, error) { + registry := resource.RegistryStr() + + // Build candidate keys to look up in the docker config auths map. + // Docker configs can use various formats: "registry.io", "https://registry.io", + // "https://registry.io/v1/", "https://registry.io/v2/", etc. + candidates := []string{ + registry, + "https://" + registry, + "https://" + registry + "/v1/", + "https://" + registry + "/v2/", + } + + for _, config := range k.configs { + for _, candidate := range candidates { + if entry, ok := config.Auths[candidate]; ok { + return resolveAuth(entry) + } + } + } + + return authn.Anonymous, nil +} + +func resolveAuth(entry DockerConfigEntry) (authn.Authenticator, error) { + if entry.Auth != "" { + decoded, err := base64.StdEncoding.DecodeString(entry.Auth) + if err != nil { + return nil, fmt.Errorf("failed to decode auth field: %w", err) + } + // Auth is base64 encoded "username:password" + parts := strings.SplitN(string(decoded), ":", 2) + if len(parts) == 2 { + return authn.FromConfig(authn.AuthConfig{ + Username: parts[0], + Password: parts[1], + }), nil + } + } + + if entry.Username != "" && entry.Password != "" { + return authn.FromConfig(authn.AuthConfig{ + Username: entry.Username, + Password: entry.Password, + }), nil + } + + return authn.Anonymous, nil } diff --git a/pkg/imagecache/imagecache_test.go b/pkg/imagecache/imagecache_test.go index 15ae77342..691ba8247 100644 --- a/pkg/imagecache/imagecache_test.go +++ b/pkg/imagecache/imagecache_test.go @@ -4,12 +4,21 @@ package imagecache import ( + "context" + "encoding/base64" + "encoding/json" "fmt" + "sync" "testing" "time" + "github.com/google/go-containerregistry/pkg/authn" + "github.com/google/go-containerregistry/pkg/name" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) const ( @@ -74,8 +83,9 @@ func TestCache(t *testing.T) { t.Run(test.name, func(t *testing.T) { // Arrange testCache := &imageCache{ - images: test.imagesMap, - fetchImage: test.fetchImageFunc, + images: test.imagesMap, + imagesMutex: &sync.RWMutex{}, + fetchImage: test.fetchImageFunc, } // Act @@ -95,3 +105,218 @@ func TestCache(t *testing.T) { }) } } + +func TestKeychainFromSecrets(t *testing.T) { + tests := []struct { + name string + secrets []corev1.Secret + secretRefs []corev1.LocalObjectReference + registry string + expectUsername string + expectPassword string + expectAnon bool + }{ + { + name: "empty secret refs returns default keychain", + secrets: nil, + secretRefs: nil, + expectAnon: true, + }, + { + name: "secret not found is skipped", + secretRefs: []corev1.LocalObjectReference{ + {Name: "nonexistent-secret"}, + }, + expectAnon: true, + }, + { + name: "dockerconfigjson with auth field", + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + Namespace: "test-ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": mustMarshalDockerConfig(t, DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{ + "ghcr.io": { + Auth: base64.StdEncoding.EncodeToString([]byte("myuser:mypass")), + }, + }, + }), + }, + }, + }, + secretRefs: []corev1.LocalObjectReference{ + {Name: "my-secret"}, + }, + registry: "ghcr.io", + expectUsername: "myuser", + expectPassword: "mypass", + }, + { + name: "dockerconfigjson with username/password fields", + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + Namespace: "test-ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": mustMarshalDockerConfig(t, DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{ + // Use index.docker.io as that's what go-containerregistry normalizes docker.io to + "index.docker.io": { + Username: "dockeruser", + Password: "dockerpass", + }, + }, + }), + }, + }, + }, + secretRefs: []corev1.LocalObjectReference{ + {Name: "my-secret"}, + }, + registry: "docker.io", + expectUsername: "dockeruser", + expectPassword: "dockerpass", + }, + { + name: "multiple secrets - first match wins", + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "secret1", + Namespace: "test-ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": mustMarshalDockerConfig(t, DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{ + "ghcr.io": { + Username: "user1", + Password: "pass1", + }, + }, + }), + }, + }, + { + ObjectMeta: metav1.ObjectMeta{ + Name: "secret2", + Namespace: "test-ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": mustMarshalDockerConfig(t, DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{ + "ghcr.io": { + Username: "user2", + Password: "pass2", + }, + }, + }), + }, + }, + }, + secretRefs: []corev1.LocalObjectReference{ + {Name: "secret1"}, + {Name: "secret2"}, + }, + registry: "ghcr.io", + expectUsername: "user1", + expectPassword: "pass1", + }, + { + name: "registry with https prefix in secret", + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + Namespace: "test-ns", + }, + Type: corev1.SecretTypeDockerConfigJson, + Data: map[string][]byte{ + ".dockerconfigjson": mustMarshalDockerConfig(t, DockerConfigJSON{ + Auths: map[string]DockerConfigEntry{ + "https://ghcr.io": { + Username: "httpsuser", + Password: "httpspass", + }, + }, + }), + }, + }, + }, + secretRefs: []corev1.LocalObjectReference{ + {Name: "my-secret"}, + }, + registry: "ghcr.io", + expectUsername: "httpsuser", + expectPassword: "httpspass", + }, + { + name: "secret with wrong data key is skipped", + secrets: []corev1.Secret{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "my-secret", + Namespace: "test-ns", + }, + Data: map[string][]byte{ + "wrongkey": []byte("somedata"), + }, + }, + }, + secretRefs: []corev1.LocalObjectReference{ + {Name: "my-secret"}, + }, + expectAnon: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Build fake client with secrets + builder := fake.NewClientBuilder() + for i := range tt.secrets { + builder = builder.WithObjects(&tt.secrets[i]) + } + kubeClient := builder.Build() + + keychain, err := KeychainFromSecrets(context.Background(), kubeClient, "test-ns", tt.secretRefs) + require.NoError(t, err) + + if tt.registry == "" { + return + } + + // Create a fake resource to resolve auth for + ref, err := name.ParseReference(tt.registry + "/test/image:latest") + require.NoError(t, err) + + auth, err := keychain.Resolve(ref.Context()) + require.NoError(t, err) + + if tt.expectAnon { + assert.Equal(t, authn.Anonymous, auth) + return + } + + authConfig, err := auth.Authorization() + require.NoError(t, err) + assert.Equal(t, tt.expectUsername, authConfig.Username) + assert.Equal(t, tt.expectPassword, authConfig.Password) + }) + } +} + +func mustMarshalDockerConfig(t *testing.T, config DockerConfigJSON) []byte { + data, err := json.Marshal(config) + require.NoError(t, err) + return data +} diff --git a/pkg/utils/k8s/proxy.go b/pkg/utils/k8s/proxy.go new file mode 100644 index 000000000..40d5212ae --- /dev/null +++ b/pkg/utils/k8s/proxy.go @@ -0,0 +1,46 @@ +// Copyright Mondoo, Inc. 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package k8s + +import ( + corev1 "k8s.io/api/core/v1" + + "go.mondoo.com/mondoo-operator/api/v1alpha2" +) + +// ProxyEnvVars returns environment variables for HTTP/HTTPS proxy configuration. +// It sets both uppercase and lowercase variants for compatibility with different tools. +func ProxyEnvVars(cfg v1alpha2.MondooOperatorConfig) []corev1.EnvVar { + var envVars []corev1.EnvVar + + if cfg.Spec.HttpProxy != nil { + envVars = append(envVars, + corev1.EnvVar{Name: "HTTP_PROXY", Value: *cfg.Spec.HttpProxy}, + corev1.EnvVar{Name: "http_proxy", Value: *cfg.Spec.HttpProxy}, + ) + } + if cfg.Spec.HttpsProxy != nil { + envVars = append(envVars, + corev1.EnvVar{Name: "HTTPS_PROXY", Value: *cfg.Spec.HttpsProxy}, + corev1.EnvVar{Name: "https_proxy", Value: *cfg.Spec.HttpsProxy}, + ) + } + if cfg.Spec.NoProxy != nil { + envVars = append(envVars, + corev1.EnvVar{Name: "NO_PROXY", Value: *cfg.Spec.NoProxy}, + corev1.EnvVar{Name: "no_proxy", Value: *cfg.Spec.NoProxy}, + ) + } + + return envVars +} + +// APIProxyURL returns the proxy URL to use for Mondoo API calls. +// Since the Mondoo API is always HTTPS, it prefers HttpsProxy over HttpProxy. +func APIProxyURL(cfg v1alpha2.MondooOperatorConfig) *string { + if cfg.Spec.HttpsProxy != nil { + return cfg.Spec.HttpsProxy + } + return cfg.Spec.HttpProxy +} diff --git a/pkg/utils/k8s/proxy_test.go b/pkg/utils/k8s/proxy_test.go new file mode 100644 index 000000000..4d760fc84 --- /dev/null +++ b/pkg/utils/k8s/proxy_test.go @@ -0,0 +1,90 @@ +// Copyright Mondoo, Inc. 2026 +// SPDX-License-Identifier: BUSL-1.1 + +package k8s + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.mondoo.com/mondoo-operator/api/v1alpha2" + "k8s.io/utils/ptr" +) + +func TestProxyEnvVars_AllSet(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + NoProxy: ptr.To("localhost,10.0.0.0/8"), + }, + } + + envVars := ProxyEnvVars(cfg) + require.Len(t, envVars, 6) + + envMap := map[string]string{} + for _, e := range envVars { + envMap[e.Name] = e.Value + } + assert.Equal(t, "http://proxy:8080", envMap["HTTP_PROXY"]) + assert.Equal(t, "http://proxy:8080", envMap["http_proxy"]) + assert.Equal(t, "https://proxy:8443", envMap["HTTPS_PROXY"]) + assert.Equal(t, "https://proxy:8443", envMap["https_proxy"]) + assert.Equal(t, "localhost,10.0.0.0/8", envMap["NO_PROXY"]) + assert.Equal(t, "localhost,10.0.0.0/8", envMap["no_proxy"]) +} + +func TestProxyEnvVars_OnlyHttpProxy(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + }, + } + + envVars := ProxyEnvVars(cfg) + require.Len(t, envVars, 2) + assert.Equal(t, "HTTP_PROXY", envVars[0].Name) + assert.Equal(t, "http_proxy", envVars[1].Name) +} + +func TestProxyEnvVars_NoneSet(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{} + + envVars := ProxyEnvVars(cfg) + assert.Empty(t, envVars) +} + +func TestAPIProxyURL_PrefersHttpsProxy(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + HttpsProxy: ptr.To("https://proxy:8443"), + }, + } + + result := APIProxyURL(cfg) + require.NotNil(t, result) + assert.Equal(t, "https://proxy:8443", *result) +} + +func TestAPIProxyURL_FallsBackToHttpProxy(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{ + Spec: v1alpha2.MondooOperatorConfigSpec{ + HttpProxy: ptr.To("http://proxy:8080"), + }, + } + + result := APIProxyURL(cfg) + require.NotNil(t, result) + assert.Equal(t, "http://proxy:8080", *result) +} + +func TestAPIProxyURL_NilWhenNoneSet(t *testing.T) { + cfg := v1alpha2.MondooOperatorConfig{} + + result := APIProxyURL(cfg) + assert.Nil(t, result) +} diff --git a/pkg/utils/mondoo/container_image_resolver.go b/pkg/utils/mondoo/container_image_resolver.go index e9b996a4c..c6c4bbe34 100644 --- a/pkg/utils/mondoo/container_image_resolver.go +++ b/pkg/utils/mondoo/container_image_resolver.go @@ -7,6 +7,7 @@ import ( "context" "fmt" "os" + "strings" "github.com/go-logr/logr" @@ -43,6 +44,18 @@ type ContainerImageResolver interface { // by a digest. If userImage, userTag, or userDigest are empty strings, default values are used. // When userDigest is specified, it takes precedence over userTag. MondooOperatorImage(ctx context.Context, userImage, userTag, userDigest string, skipImageResolution bool) (string, error) + + // WithImageRegistry returns a new ContainerImageResolver that uses the specified image registry prefix. + // Use this for simple registry mirrors where all images go to the same mirror. + WithImageRegistry(imageRegistry string) ContainerImageResolver + + // WithRegistryMirrors returns a new ContainerImageResolver that uses the specified registry mirrors. + // Use this when you need to map different source registries to different mirrors. + // The mirrors map public registries to private mirrors (e.g., "ghcr.io" -> "artifactory.example.com/ghcr.io.docker"). + WithRegistryMirrors(registryMirrors map[string]string) ContainerImageResolver + + // WithImagePullSecrets returns a new ContainerImageResolver that uses the specified imagePullSecrets for authentication. + WithImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) ContainerImageResolver } type containerImageResolver struct { @@ -52,9 +65,16 @@ type containerImageResolver struct { kubeClient client.Client operatorPodName string operatorPodNamespace string + imageRegistry string + registryMirrors map[string]string + imagePullSecrets []corev1.LocalObjectReference } func NewContainerImageResolver(kubeClient client.Client, isOpenShift bool) ContainerImageResolver { + return NewContainerImageResolverWithRegistry(kubeClient, isOpenShift, "") +} + +func NewContainerImageResolverWithRegistry(kubeClient client.Client, isOpenShift bool, imageRegistry string) ContainerImageResolver { podName := os.Getenv(PodNameEnvVar) if podName == "" { podName = "mondoo-operator-controller-manager" @@ -71,6 +91,7 @@ func NewContainerImageResolver(kubeClient client.Client, isOpenShift bool) Conta kubeClient: kubeClient, operatorPodName: podName, operatorPodNamespace: podNamespace, + imageRegistry: imageRegistry, } } @@ -89,7 +110,7 @@ func (c *containerImageResolver) CnspecImage(userImage, userTag, userDigest stri skipImageResolution = true } - return c.resolveImage(image, skipImageResolution) + return c.resolveImage(context.Background(), image, skipImageResolution) } func (c *containerImageResolver) MondooOperatorImage(ctx context.Context, userImage, userTag, userDigest string, skipImageResolution bool) (string, error) { @@ -123,15 +144,27 @@ func (c *containerImageResolver) MondooOperatorImage(ctx context.Context, userIm skipImageResolution = true } - return c.resolveImage(image, skipImageResolution) + return c.resolveImage(ctx, image, skipImageResolution) } -func (c *containerImageResolver) resolveImage(image string, skipImageResolution bool) (string, error) { +func (c *containerImageResolver) resolveImage(ctx context.Context, image string, skipImageResolution bool) (string, error) { + // Apply custom image registry prefix if configured + image = c.applyImageRegistry(image) + if skipImageResolution { return image, nil } - imageWithDigest, err := c.imageCacher.GetImage(image) + // Apply authentication if imagePullSecrets are configured + cacher := c.imageCacher + if len(c.imagePullSecrets) > 0 { + keychain, err := imagecache.KeychainFromSecrets(ctx, c.kubeClient, c.operatorPodNamespace, c.imagePullSecrets) + if err == nil { + cacher = cacher.WithAuth(keychain) + } + } + + imageWithDigest, err := cacher.GetImage(image) if err != nil { c.logger.Error(err, "failed to resolve image plus digest") return "", err @@ -140,6 +173,102 @@ func (c *containerImageResolver) resolveImage(image string, skipImageResolution return imageWithDigest, nil } +// applyImageRegistry rewrites the image to use a custom registry if configured. +// It first checks registryMirrors for a specific mapping, then falls back to imageRegistry. +// For example, if registryMirrors has "ghcr.io" -> "artifactory.example.com/ghcr.io.docker" and +// the image is "ghcr.io/mondoohq/mondoo-operator:v1.0.0", it will be rewritten to +// "artifactory.example.com/ghcr.io.docker/mondoohq/mondoo-operator:v1.0.0" +func (c *containerImageResolver) applyImageRegistry(image string) string { + // Parse the image to extract registry, repository, and tag + parts := splitImageParts(image) + + // First, check if we have a specific mirror for this registry + if len(c.registryMirrors) > 0 && parts.registry != "" { + if mirror, ok := c.registryMirrors[parts.registry]; ok { + return fmt.Sprintf("%s/%s", mirror, parts.repositoryWithTag) + } + } + + // Fall back to the legacy imageRegistry if set + if c.imageRegistry == "" { + return image + } + + if parts.registry != "" { + // Replace the registry with the custom one + return fmt.Sprintf("%s/%s", c.imageRegistry, parts.repositoryWithTag) + } + + // No registry in the image, just prepend the custom registry + return fmt.Sprintf("%s/%s", c.imageRegistry, image) +} + +type imageParts struct { + registry string + repositoryWithTag string +} + +func splitImageParts(image string) imageParts { + // Find the first slash + slashIdx := -1 + for i, c := range image { + if c == '/' { + slashIdx = i + break + } + } + + if slashIdx == -1 { + // No slash, no registry (e.g., "ubuntu:latest") + return imageParts{registry: "", repositoryWithTag: image} + } + + potentialRegistry := image[:slashIdx] + // Check if it looks like a registry (contains a dot or colon, or is "localhost") + if strings.Contains(potentialRegistry, ".") || strings.Contains(potentialRegistry, ":") || potentialRegistry == "localhost" { + return imageParts{ + registry: potentialRegistry, + repositoryWithTag: image[slashIdx+1:], + } + } + + // No registry, the first part is part of the repository (e.g., "library/ubuntu:latest") + return imageParts{registry: "", repositoryWithTag: image} +} + +// clone creates a shallow copy of the resolver +func (c *containerImageResolver) clone() *containerImageResolver { + return &containerImageResolver{ + logger: c.logger, + resolveForOpenShift: c.resolveForOpenShift, + imageCacher: c.imageCacher, + kubeClient: c.kubeClient, + operatorPodName: c.operatorPodName, + operatorPodNamespace: c.operatorPodNamespace, + imageRegistry: c.imageRegistry, + registryMirrors: c.registryMirrors, + imagePullSecrets: c.imagePullSecrets, + } +} + +func (c *containerImageResolver) WithImageRegistry(imageRegistry string) ContainerImageResolver { + clone := c.clone() + clone.imageRegistry = imageRegistry + return clone +} + +func (c *containerImageResolver) WithRegistryMirrors(registryMirrors map[string]string) ContainerImageResolver { + clone := c.clone() + clone.registryMirrors = registryMirrors + return clone +} + +func (c *containerImageResolver) WithImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) ContainerImageResolver { + clone := c.clone() + clone.imagePullSecrets = imagePullSecrets + return clone +} + func userImageOrDefault(defaultImage, defaultTag, userImage, userTag, userDigest string) string { image := defaultImage if userImage != "" { diff --git a/pkg/utils/mondoo/container_image_resolver_test.go b/pkg/utils/mondoo/container_image_resolver_test.go index 16260f9f0..88553f38a 100644 --- a/pkg/utils/mondoo/container_image_resolver_test.go +++ b/pkg/utils/mondoo/container_image_resolver_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" "github.com/stretchr/testify/suite" @@ -16,6 +17,8 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "go.mondoo.com/mondoo-operator/pkg/imagecache" ) type ContainerImageResolverSuite struct { @@ -33,6 +36,10 @@ func (f *fakeCacher) GetImage(img string) (string, error) { return f.fakeGetImage(img) } +func (f *fakeCacher) WithAuth(keychain authn.Keychain) imagecache.ImageCacher { + return f // Return itself since we don't need auth in tests +} + func NewFakeCacher(f func(string) (string, error)) *fakeCacher { return &fakeCacher{ fakeGetImage: f, @@ -241,3 +248,157 @@ func (s *ContainerImageResolverSuite) TestMondooOperatorImage_DigestWithTag() { func TestContainerImageResolverSuite(t *testing.T) { suite.Run(t, new(ContainerImageResolverSuite)) } + +func TestSplitImageParts(t *testing.T) { + tests := []struct { + name string + image string + expectedRegistry string + expectedRepoTag string + }{ + { + name: "ghcr.io image", + image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + expectedRegistry: "ghcr.io", + expectedRepoTag: "mondoohq/mondoo-operator:v1.0.0", + }, + { + name: "docker.io image", + image: "docker.io/library/nginx:latest", + expectedRegistry: "docker.io", + expectedRepoTag: "library/nginx:latest", + }, + { + name: "quay.io image", + image: "quay.io/prometheus/prometheus:v2.40.0", + expectedRegistry: "quay.io", + expectedRepoTag: "prometheus/prometheus:v2.40.0", + }, + { + name: "private registry with port", + image: "registry.example.com:5000/myimage:tag", + expectedRegistry: "registry.example.com:5000", + expectedRepoTag: "myimage:tag", + }, + { + name: "localhost registry", + image: "localhost/myimage:tag", + expectedRegistry: "localhost", + expectedRepoTag: "myimage:tag", + }, + { + name: "localhost with port", + image: "localhost:5000/myimage:tag", + expectedRegistry: "localhost:5000", + expectedRepoTag: "myimage:tag", + }, + { + name: "image without registry (library image)", + image: "nginx:latest", + expectedRegistry: "", + expectedRepoTag: "nginx:latest", + }, + { + name: "image without registry (org/repo)", + image: "myorg/myimage:tag", + expectedRegistry: "", + expectedRepoTag: "myorg/myimage:tag", + }, + { + name: "image with digest", + image: "ghcr.io/mondoohq/cnspec@sha256:abc123", + expectedRegistry: "ghcr.io", + expectedRepoTag: "mondoohq/cnspec@sha256:abc123", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + parts := splitImageParts(tt.image) + if parts.registry != tt.expectedRegistry { + t.Errorf("registry: got %q, want %q", parts.registry, tt.expectedRegistry) + } + if parts.repositoryWithTag != tt.expectedRepoTag { + t.Errorf("repositoryWithTag: got %q, want %q", parts.repositoryWithTag, tt.expectedRepoTag) + } + }) + } +} + +func TestApplyImageRegistry(t *testing.T) { + tests := []struct { + name string + image string + imageRegistry string + registryMirrors map[string]string + expected string + }{ + { + name: "no registry configured", + image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + imageRegistry: "", + expected: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + }, + { + name: "imageRegistry replaces registry", + image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + imageRegistry: "artifactory.example.com/ghcr.io.docker", + expected: "artifactory.example.com/ghcr.io.docker/mondoohq/mondoo-operator:v1.0.0", + }, + { + name: "registryMirrors replaces specific registry", + image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + registryMirrors: map[string]string{ + "ghcr.io": "artifactory.example.com/ghcr.io.docker", + }, + expected: "artifactory.example.com/ghcr.io.docker/mondoohq/mondoo-operator:v1.0.0", + }, + { + name: "registryMirrors takes precedence over imageRegistry", + image: "ghcr.io/mondoohq/mondoo-operator:v1.0.0", + registryMirrors: map[string]string{ + "ghcr.io": "mirror.example.com/ghcr", + }, + imageRegistry: "fallback.example.com", + expected: "mirror.example.com/ghcr/mondoohq/mondoo-operator:v1.0.0", + }, + { + name: "registryMirrors does not match - falls back to imageRegistry", + image: "quay.io/prometheus/prometheus:v2.40.0", + registryMirrors: map[string]string{ + "ghcr.io": "mirror.example.com/ghcr", + }, + imageRegistry: "fallback.example.com", + expected: "fallback.example.com/prometheus/prometheus:v2.40.0", + }, + { + name: "multiple registryMirrors", + image: "docker.io/library/nginx:latest", + registryMirrors: map[string]string{ + "ghcr.io": "mirror.example.com/ghcr", + "docker.io": "mirror.example.com/dockerhub", + "quay.io": "mirror.example.com/quay", + }, + expected: "mirror.example.com/dockerhub/library/nginx:latest", + }, + { + name: "imageRegistry with library image (no registry)", + image: "nginx:latest", + imageRegistry: "mirror.example.com", + expected: "mirror.example.com/nginx:latest", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + resolver := &containerImageResolver{ + imageRegistry: tt.imageRegistry, + registryMirrors: tt.registryMirrors, + } + result := resolver.applyImageRegistry(tt.image) + if result != tt.expected { + t.Errorf("got %q, want %q", result, tt.expected) + } + }) + } +} diff --git a/pkg/utils/mondoo/fake/container_image_resolver.go b/pkg/utils/mondoo/fake/container_image_resolver.go index 5d77a9811..ab714ceeb 100644 --- a/pkg/utils/mondoo/fake/container_image_resolver.go +++ b/pkg/utils/mondoo/fake/container_image_resolver.go @@ -7,6 +7,8 @@ import ( "context" "fmt" + corev1 "k8s.io/api/core/v1" + "go.mondoo.com/mondoo-operator/pkg/utils/mondoo" ) @@ -65,3 +67,27 @@ func (c *ContainerImageResolverMock) MondooOperatorImage(ctx context.Context, us } return fmt.Sprintf("%s:%s", mondoo.MondooOperatorImage, mondoo.MondooOperatorTag), nil } + +func (c *noOpContainerImageResolver) WithImageRegistry(imageRegistry string) mondoo.ContainerImageResolver { + return c +} + +func (c *noOpContainerImageResolver) WithRegistryMirrors(registryMirrors map[string]string) mondoo.ContainerImageResolver { + return c +} + +func (c *noOpContainerImageResolver) WithImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) mondoo.ContainerImageResolver { + return c +} + +func (c *ContainerImageResolverMock) WithImageRegistry(imageRegistry string) mondoo.ContainerImageResolver { + return c +} + +func (c *ContainerImageResolverMock) WithRegistryMirrors(registryMirrors map[string]string) mondoo.ContainerImageResolver { + return c +} + +func (c *ContainerImageResolverMock) WithImagePullSecrets(imagePullSecrets []corev1.LocalObjectReference) mondoo.ContainerImageResolver { + return c +} diff --git a/pkg/utils/mondoo/integration.go b/pkg/utils/mondoo/integration.go index 7a773a06e..eba5564dc 100644 --- a/pkg/utils/mondoo/integration.go +++ b/pkg/utils/mondoo/integration.go @@ -17,6 +17,8 @@ func IntegrationCheckIn( sa mondooclient.ServiceAccountCredentials, mondooClientBuilder MondooClientBuilder, httpProxy *string, + httpsProxy *string, + noProxy *string, logger logr.Logger, ) error { token, err := GenerateTokenFromServiceAccount(sa, logger) @@ -28,6 +30,8 @@ func IntegrationCheckIn( ApiEndpoint: sa.ApiEndpoint, Token: token, HttpProxy: httpProxy, + HttpsProxy: httpsProxy, + NoProxy: noProxy, }) if err != nil { return err diff --git a/pkg/utils/mondoo/token_exchange.go b/pkg/utils/mondoo/token_exchange.go index 3932a07e3..85d1d01d8 100644 --- a/pkg/utils/mondoo/token_exchange.go +++ b/pkg/utils/mondoo/token_exchange.go @@ -26,7 +26,7 @@ type MondooClientBuilder func(mondooclient.MondooClientOptions) (mondooclient.Mo // CreateServiceAccountFromToken will take the provided Mondoo token and exchange it with the Mondoo API // for a long lived Mondoo ServiceAccount -func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client, mondooClientBuilder MondooClientBuilder, withConsoleIntegration bool, serviceAccountSecret types.NamespacedName, tokenSecretData string, httpProxy *string, log logr.Logger) error { +func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client, mondooClientBuilder MondooClientBuilder, withConsoleIntegration bool, serviceAccountSecret types.NamespacedName, tokenSecretData string, httpProxy *string, httpsProxy *string, noProxy *string, log logr.Logger) error { jwtString := strings.TrimSpace(tokenSecretData) parser := &jwt.Parser{} @@ -48,6 +48,8 @@ func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client ApiEndpoint: fmt.Sprintf("%v", apiEndpoint), Token: jwtString, HttpProxy: httpProxy, + HttpsProxy: httpsProxy, + NoProxy: noProxy, } mClient, err := mondooClientBuilder(opts) @@ -98,7 +100,7 @@ func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client // No easy way to retry this one-off CheckIn(). An error on initial CheckIn() // means we'll just retry on the regularly scheduled interval via the integration controller - _ = performInitialCheckIn(ctx, mondooClientBuilder, integrationMrn, *resp.Creds, httpProxy, log) + _ = performInitialCheckIn(ctx, mondooClientBuilder, integrationMrn, *resp.Creds, httpProxy, httpsProxy, noProxy, log) } else { // Do a vanilla token-for-service-account exchange resp, err := mClient.ExchangeRegistrationToken(ctx, &mondooclient.ExchangeRegistrationTokenInput{ @@ -125,8 +127,8 @@ func CreateServiceAccountFromToken(ctx context.Context, kubeClient client.Client return nil } -func performInitialCheckIn(ctx context.Context, mondooClientBuilder MondooClientBuilder, integrationMrn string, sa mondooclient.ServiceAccountCredentials, httpProxy *string, logger logr.Logger) error { - if err := IntegrationCheckIn(ctx, integrationMrn, sa, mondooClientBuilder, httpProxy, logger); err != nil { +func performInitialCheckIn(ctx context.Context, mondooClientBuilder MondooClientBuilder, integrationMrn string, sa mondooclient.ServiceAccountCredentials, httpProxy *string, httpsProxy *string, noProxy *string, logger logr.Logger) error { + if err := IntegrationCheckIn(ctx, integrationMrn, sa, mondooClientBuilder, httpProxy, httpsProxy, noProxy, logger); err != nil { logger.Error(err, "initial CheckIn() failed, will CheckIn() periodically", "integrationMRN", integrationMrn) return err } diff --git a/tests/framework/utils/audit_config.go b/tests/framework/utils/audit_config.go index 75c7f32a2..4e1720cbf 100644 --- a/tests/framework/utils/audit_config.go +++ b/tests/framework/utils/audit_config.go @@ -37,7 +37,11 @@ func init() { // DefaultAuditConfig instead. func DefaultAuditConfigMinimal(ns string, workloads, containers, nodes bool) mondoov2.MondooAuditConfig { now := time.Now() - startScan := now.Add(time.Minute).Add(time.Second * 15) + // The cron schedule only uses the minute field, so the real buffer is + // (targetMinuteStart - now). With a 2m offset the minimum buffer is ~61s, + // which safely covers leader election (~45s) plus CronJob creation time, + // while keeping the worst-case trigger (~120s) within the retry window. + startScan := now.Add(2 * time.Minute) schedule := fmt.Sprintf("%d * * * *", startScan.Minute()) auditConfig := mondoov2.MondooAuditConfig{ TypeMeta: v1.TypeMeta{ diff --git a/tests/framework/utils/helm_helper.go b/tests/framework/utils/helm_helper.go index 799305591..d1c073a01 100644 --- a/tests/framework/utils/helm_helper.go +++ b/tests/framework/utils/helm_helper.go @@ -84,6 +84,7 @@ func (h *HelmHelper) Template(releaseName, chartPath, namespace string, values m releaseName, chartPath, "--namespace", namespace, + "--include-crds", } for k, v := range values { diff --git a/tests/framework/utils/k8s_helper.go b/tests/framework/utils/k8s_helper.go index 044b4b595..0ab0cf6a4 100644 --- a/tests/framework/utils/k8s_helper.go +++ b/tests/framework/utils/k8s_helper.go @@ -43,7 +43,8 @@ import ( const ( cmd = "kubectl" RetryInterval = 2 - RetryLoop = 50 + RetryLoop = 100 + CronJobRetryLoop = 300 // CronJobs need more time: schedule delay + scan execution (600s) SkipVersionCheckEnvVar = "SKIP_VERSION_CHECK" ) @@ -221,7 +222,8 @@ func (k8sh *K8sHelper) WaitUntilMondooClientSecretExists(ctx context.Context, ns } // WaitUntilCronJobsSuccessful waits for the CronJobs with the specified selector to have at least -// one successful run. +// one successful run. Uses CronJobRetryLoop for a longer timeout since CronJobs need time for +// schedule delay plus scan execution. func (k8sh *K8sHelper) WaitUntilCronJobsSuccessful(labelSelector, namespace string) bool { listOpts, err := LabelSelectorListOptions(labelSelector) if err != nil { @@ -231,25 +233,26 @@ func (k8sh *K8sHelper) WaitUntilCronJobsSuccessful(labelSelector, namespace stri ctx := context.Background() cronJobs := &batchv1.CronJobList{} - err = k8sh.ExecuteWithRetries(func() (bool, error) { + for i := 0; i < CronJobRetryLoop; i++ { if err := k8sh.Clientset.List(ctx, cronJobs, listOpts); err != nil { zap.S().Errorf("Failed to list CronJobs. %v", err) - return false, err + return false } + + allReady := true for _, c := range cronJobs.Items { - // Make sure the job has been scheduled and is not active if c.Status.LastScheduleTime == nil || len(c.Status.Active) > 0 { - return false, nil + allReady = false + break } } - // Make sure all jobs have succeeded - if k8s.AreCronJobsSuccessful(cronJobs.Items) { - return true, nil + if allReady && k8s.AreCronJobsSuccessful(cronJobs.Items) { + return true } - return false, nil - }) - return err == nil + time.Sleep(RetryInterval * time.Second) + } + return false } func LabelSelectorListOptions(labelSelector string) (*client.ListOptions, error) { @@ -505,7 +508,8 @@ func (k8sh *K8sHelper) getPodDescribe(namespace string, args ...string) string { args = append([]string{"get", "pod", "-o", "yaml", "-n", namespace}, args...) description, err := k8sh.Kubectl(args...) if err != nil { - zap.S().Errorf("failed to describe pod. %v %+v", args, err) + // Completed CronJob pods may be cleaned up before we can describe them + zap.S().Warnf("failed to describe pod. %v %+v", args, err) return "" } return description diff --git a/tests/integration/audit_config_base_suite.go b/tests/integration/audit_config_base_suite.go index 1b0fee2ab..5c4454a53 100644 --- a/tests/integration/audit_config_base_suite.go +++ b/tests/integration/audit_config_base_suite.go @@ -120,7 +120,7 @@ func (s *AuditConfigBaseSuite) AfterTest(suiteName, testName string) { _, err = s.testCluster.K8sHelper.Kubectl("delete", "deployments", "-n", "default", "--all", "--ignore-not-found", "--wait") s.Require().NoError(err, "Failed to delete all deployments in default namespace") - _, err = s.testCluster.K8sHelper.Kubectl("delete", "pods", "-n", "default", "--all", "--wait") + _, err = s.testCluster.K8sHelper.Kubectl("delete", "pods", "-n", "default", "--all", "--ignore-not-found", "--wait") s.Require().NoError(err, "Failed to delete all pods") } } diff --git a/tests/integration/external_cluster_test.go b/tests/integration/external_cluster_test.go index cbb127a8d..ac762bb90 100644 --- a/tests/integration/external_cluster_test.go +++ b/tests/integration/external_cluster_test.go @@ -159,6 +159,9 @@ func (s *ExternalClusterSuite) switchToManagementContext() error { } func (s *ExternalClusterSuite) createTargetCluster() error { + // Delete any stale target cluster from a previous run + s.cleanupTargetCluster() + cmd := exec.Command("k3d", "cluster", "create", targetClusterName, "--api-port", "6444") output, err := cmd.CombinedOutput() if err != nil {