go.mondoo.com/cnquery@v0.0.0-20231005093811-59568235f6ea/_motor/discovery/k8s/list_workloads.go (about)

     1  // Copyright (c) Mondoo, Inc.
     2  // SPDX-License-Identifier: BUSL-1.1
     3  
     4  package k8s
     5  
     6  import (
     7  	"fmt"
     8  
     9  	"github.com/gobwas/glob"
    10  	"github.com/pkg/errors"
    11  	"github.com/rs/zerolog/log"
    12  	"go.mondoo.com/cnquery/motor/asset"
    13  	"go.mondoo.com/cnquery/motor/providers"
    14  	"go.mondoo.com/cnquery/motor/providers/k8s"
    15  	v1 "k8s.io/api/core/v1"
    16  	k8sErrors "k8s.io/apimachinery/pkg/api/errors"
    17  	"k8s.io/apimachinery/pkg/api/meta"
    18  	"k8s.io/apimachinery/pkg/runtime"
    19  )
    20  
    21  type NamespaceFilterOpts struct {
    22  	include []string
    23  	exclude []string
    24  }
    25  
    26  // ListCronJobs list all cronjobs in the cluster.
    27  func ListCronJobs(
    28  	p k8s.KubernetesProvider,
    29  	connection *providers.Config,
    30  	clusterIdentifier string,
    31  	nsFilter NamespaceFilterOpts,
    32  	resFilter map[string][]K8sResourceIdentifier,
    33  	od *k8s.PlatformIdOwnershipDirectory,
    34  ) ([]*asset.Asset, error) {
    35  	return ListNamespacedObj(p, connection, clusterIdentifier, nsFilter, resFilter, od, "cronjob", p.CronJob, p.CronJobs)
    36  }
    37  
    38  func ListDaemonSets(
    39  	p k8s.KubernetesProvider,
    40  	connection *providers.Config,
    41  	clusterIdentifier string,
    42  	nsFilter NamespaceFilterOpts,
    43  	resFilter map[string][]K8sResourceIdentifier,
    44  	od *k8s.PlatformIdOwnershipDirectory,
    45  ) ([]*asset.Asset, error) {
    46  	return ListNamespacedObj(p, connection, clusterIdentifier, nsFilter, resFilter, od, "daemonset", p.DaemonSet, p.DaemonSets)
    47  }
    48  
    49  // ListDeployments lits all deployments in the cluster.
    50  func ListDeployments(
    51  	p k8s.KubernetesProvider,
    52  	connection *providers.Config,
    53  	clusterIdentifier string,
    54  	nsFilter NamespaceFilterOpts,
    55  	resFilter map[string][]K8sResourceIdentifier,
    56  	od *k8s.PlatformIdOwnershipDirectory,
    57  ) ([]*asset.Asset, error) {
    58  	return ListNamespacedObj(p, connection, clusterIdentifier, nsFilter, resFilter, od, "deployment", p.Deployment, p.Deployments)
    59  }
    60  
    61  // ListJobs list all jobs in the cluster.
    62  func ListJobs(
    63  	p k8s.KubernetesProvider,
    64  	connection *providers.Config,
    65  	clusterIdentifier string,
    66  	nsFilter NamespaceFilterOpts,
    67  	resFilter map[string][]K8sResourceIdentifier,
    68  	od *k8s.PlatformIdOwnershipDirectory,
    69  ) ([]*asset.Asset, error) {
    70  	return ListNamespacedObj(p, connection, clusterIdentifier, nsFilter, resFilter, od, "job", p.Job, p.Jobs)
    71  }
    72  
    73  // ListPods list all pods in the cluster.
    74  func ListPods(
    75  	p k8s.KubernetesProvider,
    76  	connection *providers.Config,
    77  	clusterIdentifier string,
    78  	nsFilter NamespaceFilterOpts,
    79  	resFilter map[string][]K8sResourceIdentifier,
    80  	od *k8s.PlatformIdOwnershipDirectory,
    81  ) ([]*asset.Asset, error) {
    82  	return ListNamespacedObj(p, connection, clusterIdentifier, nsFilter, resFilter, od, "pod", p.Pod, p.Pods)
    83  }
    84  
    85  // ListReplicaSets list all replicaSets in the cluster.
    86  func ListReplicaSets(
    87  	p k8s.KubernetesProvider,
    88  	connection *providers.Config,
    89  	clusterIdentifier string,
    90  	nsFilter NamespaceFilterOpts,
    91  	resFilter map[string][]K8sResourceIdentifier,
    92  	od *k8s.PlatformIdOwnershipDirectory,
    93  ) ([]*asset.Asset, error) {
    94  	return ListNamespacedObj(p, connection, clusterIdentifier, nsFilter, resFilter, od, "replicaset", p.ReplicaSet, p.ReplicaSets)
    95  }
    96  
    97  // ListStatefulSets list all statefulsets in the cluster.
    98  func ListStatefulSets(
    99  	p k8s.KubernetesProvider,
   100  	connection *providers.Config,
   101  	clusterIdentifier string,
   102  	nsFilter NamespaceFilterOpts,
   103  	resFilter map[string][]K8sResourceIdentifier,
   104  	od *k8s.PlatformIdOwnershipDirectory,
   105  ) ([]*asset.Asset, error) {
   106  	return ListNamespacedObj(p, connection, clusterIdentifier, nsFilter, resFilter, od, "statefulset", p.StatefulSet, p.StatefulSets)
   107  }
   108  
   109  func ListNamespacedObj[T runtime.Object](
   110  	p k8s.KubernetesProvider,
   111  	connection *providers.Config,
   112  	clusterIdentifier string,
   113  	nsFilter NamespaceFilterOpts,
   114  	resFilter map[string][]K8sResourceIdentifier,
   115  	od *k8s.PlatformIdOwnershipDirectory,
   116  	workloadType string,
   117  	getter func(string, string) (T, error),
   118  	lister func(v1.Namespace) ([]T, error),
   119  ) ([]*asset.Asset, error) {
   120  	workloads := []T{}
   121  
   122  	if len(resFilter) > 0 {
   123  		// If there is a resources filter we should only retrieve the workloads that are in the filter.
   124  		if len(resFilter[workloadType]) == 0 {
   125  			return []*asset.Asset{}, nil
   126  		}
   127  
   128  		for _, res := range resFilter[workloadType] {
   129  			ds, err := getter(res.Namespace, res.Name)
   130  			if err != nil {
   131  				return nil, errors.Wrapf(err, "failed to get %s %s/%s", workloadType, res.Namespace, res.Name)
   132  			}
   133  
   134  			workloads = append(workloads, ds)
   135  		}
   136  	} else {
   137  		namespaces, err := p.Namespaces()
   138  		if err != nil {
   139  			// If we don't have rights to list the cluster namespaces, attempt getting them 1 by 1
   140  			if k8sErrors.IsForbidden(err) && len(nsFilter.include) > 0 {
   141  				for _, ns := range nsFilter.include {
   142  					n, err := p.Namespace(ns)
   143  					if err != nil {
   144  						return nil, err
   145  					}
   146  					namespaces = append(namespaces, *n)
   147  				}
   148  			} else {
   149  				return nil, errors.Wrap(err, "could not list kubernetes namespaces")
   150  			}
   151  		}
   152  
   153  		for i := range namespaces {
   154  			namespace := namespaces[i]
   155  			skip, err := skipNamespace(namespace, nsFilter)
   156  			if err != nil {
   157  				log.Error().Err(err).Str("namespace", namespace.Name).Msg("error checking whether Namespace should be included or excluded")
   158  				return nil, err
   159  			}
   160  			if skip {
   161  				log.Debug().Str("namespace", namespace.Name).Msg("ignoring namespace")
   162  				continue
   163  			}
   164  
   165  			workloadsPerNamespace, err := lister(namespace)
   166  			if err != nil {
   167  				return nil, errors.Wrap(err, fmt.Sprintf("failed to list %ss", workloadType))
   168  			}
   169  			workloads = append(workloads, workloadsPerNamespace...)
   170  		}
   171  	}
   172  
   173  	assetsIdx := map[string]*asset.Asset{}
   174  	for i := range workloads {
   175  		od.Add(workloads[i])
   176  
   177  		asset, err := createAssetFromObject(workloads[i], p.Runtime(), connection, clusterIdentifier)
   178  		if err != nil {
   179  			return nil, errors.Wrap(err, fmt.Sprintf("failed to create asset from %s", workloadType))
   180  		}
   181  
   182  		// An error can never happen because of the type constraint.
   183  		obj, _ := meta.Accessor(workloads[i])
   184  		log.Debug().Str("name", obj.GetName()).Str("connection", asset.Connections[0].Host).Msgf("resolved %s", workloadType)
   185  
   186  		assetsIdx[asset.PlatformIds[0]] = asset
   187  	}
   188  
   189  	// Return a unique list of assets. Manifests can contain a namespaces that is an empty string. When we try to list k8s
   190  	// resources for the empty namespace, that actually means list all resources. Therefore we can have duplicate entries in the list.
   191  	// Here we just return only the unique assets to make sure the code works correctly with both manifests and k8s API.
   192  	assets := make([]*asset.Asset, 0, len(assetsIdx))
   193  	for k := range assetsIdx {
   194  		assets = append(assets, assetsIdx[k])
   195  	}
   196  
   197  	return assets, nil
   198  }
   199  
   200  func skipNamespace(namespace v1.Namespace, filter NamespaceFilterOpts) (bool, error) {
   201  	// anything explicitly specified in the list of includes means accept only from that list
   202  	if len(filter.include) > 0 {
   203  		for _, ns := range filter.include {
   204  			g, err := glob.Compile(ns)
   205  			if err != nil {
   206  				return false, err
   207  			}
   208  			if g.Match(namespace.Name) {
   209  				// stop looking, we found our match
   210  				return false, nil
   211  			}
   212  		}
   213  
   214  		// didn't find it, so it must be skipped
   215  		return true, nil
   216  	}
   217  
   218  	// if nothing explicitly meant to be included, then check whether
   219  	// it should be excluded
   220  	for _, ns := range filter.exclude {
   221  		g, err := glob.Compile(ns)
   222  		if err != nil {
   223  			return false, err
   224  		}
   225  		if g.Match(namespace.Name) {
   226  			return true, nil
   227  		}
   228  	}
   229  
   230  	return false, nil
   231  }