241 lines
7.3 KiB
Go
241 lines
7.3 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"iter"
|
|
"k8s.io/api/core/v1"
|
|
"maps"
|
|
"os"
|
|
"slices"
|
|
"strconv"
|
|
)
|
|
|
|
type ValidationLevel int
|
|
|
|
const (
|
|
Info ValidationLevel = iota
|
|
Warning
|
|
Error
|
|
)
|
|
|
|
type Resolver interface {
|
|
ServiceAccounts(application *Application) []string
|
|
PortNumbers(application *Application) []Port
|
|
}
|
|
|
|
func LogValidationMsg(level ValidationLevel, msg string, v ...any) {
|
|
fmt.Fprintf(os.Stderr, "NOTICE: "+msg+"\n", v...)
|
|
}
|
|
|
|
func IterToSlice[K any](i iter.Seq[K]) []K {
|
|
res := make([]K, 0)
|
|
for v := range i {
|
|
res = append(res, v)
|
|
}
|
|
return res
|
|
}
|
|
|
|
func MapKeys[K comparable, V any](m map[K]V) []K {
|
|
return IterToSlice(maps.Keys(m))
|
|
}
|
|
func MapValues[K comparable, V any](m map[K]V) []V {
|
|
return IterToSlice(maps.Values(m))
|
|
}
|
|
|
|
func validate(files []string, options *Options) error {
|
|
clientset, _ := GetKubernetesConnection()
|
|
config, err := readConfig(files)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cluster, err := NewCluster(clientset)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
config.Infer(cluster)
|
|
|
|
// map applname1 -> appname2 where appname1 is in an open namespace and app2 is in a closed namespace.
|
|
// Exclusing when 'from' side is a CIDR.
|
|
openToClosedAccess := make(map[string]string)
|
|
|
|
applicationPods := make(map[string][]v1.Pod)
|
|
for _, ns := range config.Namespaces {
|
|
namespace := ns.Name
|
|
|
|
if cluster.Namespace(namespace).Name != namespace {
|
|
LogValidationMsg(Error, "ERROR: namespace not found: %s", namespace)
|
|
continue
|
|
}
|
|
if !ns.Open {
|
|
podsNotPartOfAnyApplication(cluster, namespace, ns)
|
|
}
|
|
|
|
// checking for service accounts shared by applications
|
|
// map of namespace/sa -> []applicationname
|
|
serviceAccountMap := make(map[string][]string)
|
|
|
|
for _, application := range ns.Applications {
|
|
pods := cluster.Pods(application)
|
|
applicationPods[application.Name] = pods
|
|
//log.Printf(namespace + "/" + application.Name)
|
|
if len(pods) == 0 {
|
|
LogValidationMsg(Error, "application %s: no running pods found", application.Name)
|
|
}
|
|
ownerReferences := cluster.OwnerReferences(application)
|
|
if len(ownerReferences) > 1 {
|
|
LogValidationMsg(Error, "Application %s: multiple owners found: %v. The application definition can possibly be made more fine-grain", application.Name, ownerReferences)
|
|
}
|
|
// check ports
|
|
for _, port := range application.Ports {
|
|
for _, pod := range pods {
|
|
if !HasPort(pod, port) {
|
|
LogValidationMsg(Error, "application %s: port %v not found in pod %s/%s",
|
|
application.Name, port, pod.Namespace, pod.Name)
|
|
}
|
|
}
|
|
}
|
|
// Check service accounts
|
|
applicationServiceAccounts := make(map[string]bool)
|
|
for _, sa := range application.ServiceAccounts {
|
|
applicationServiceAccounts[sa] = true
|
|
}
|
|
for _, pod := range pods {
|
|
delete(applicationServiceAccounts, pod.Spec.ServiceAccountName)
|
|
}
|
|
if len(applicationServiceAccounts) > 0 {
|
|
LogValidationMsg(Error, "application %s: service accounts %v configured but not used by running workloads",
|
|
application.Name, MapKeys(applicationServiceAccounts))
|
|
}
|
|
for _, pod := range pods {
|
|
sa := pod.Namespace + "/" + pod.Spec.ServiceAccountName
|
|
if !slices.Contains(serviceAccountMap[sa], application.Name) {
|
|
serviceAccountMap[sa] = append(serviceAccountMap[sa],
|
|
application.Name)
|
|
}
|
|
if pod.Spec.ServiceAccountName == "default" {
|
|
LogValidationMsg(Warning, "Pod %s/%s: running with default service account",
|
|
pod.Namespace, pod.Name)
|
|
if application.ServiceAccounts != nil && !slices.Contains(application.ServiceAccounts, "default") {
|
|
LogValidationMsg(Warning, "application %s: Pod %s/%s: running with 'default' service account but configured with %v",
|
|
application.Name, pod.Namespace, pod.Name, application.ServiceAccounts)
|
|
}
|
|
} else {
|
|
if !slices.Contains(application.ServiceAccounts, pod.Spec.ServiceAccountName) {
|
|
LogValidationMsg(Warning, "application %s: Pod %s/%s: running with service account '%s' but configured with %v",
|
|
application.Name, pod.Namespace, pod.Name, pod.Spec.ServiceAccountName, application.ServiceAccounts)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// service accounts shared by multiple applications.
|
|
for sa, applist := range serviceAccountMap {
|
|
if len(applist) == 1 {
|
|
continue
|
|
}
|
|
LogValidationMsg(Error, "service account %s: shared by multiple applications %v, the application definition can be made more fine-grain.", sa, applist)
|
|
}
|
|
}
|
|
|
|
for _, communication := range config.Communications {
|
|
if len(communication.Ports) == 0 {
|
|
continue
|
|
}
|
|
for _, applicationName := range communication.To {
|
|
application, _, _ := config.GetApplication(applicationName)
|
|
if application == nil {
|
|
continue
|
|
}
|
|
for _, port := range communication.Ports {
|
|
pods := cluster.Pods(application)
|
|
for _, pod := range pods {
|
|
if !HasPort(pod, port) {
|
|
LogValidationMsg(Error, "communication %v -> %v: port %v is not configured in pod %s/%s",
|
|
communication.From, communication.To, port, pod.Namespace, pod.Name)
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
for _, communication := range config.Communications {
|
|
for _, applicationName := range communication.To {
|
|
application, _, _ := config.GetApplication(applicationName)
|
|
if application == nil {
|
|
continue
|
|
}
|
|
// capability linkerd must exist on target namespace
|
|
if !slices.Contains(application.Namespace.Capabilities, "linkerd") {
|
|
continue
|
|
}
|
|
// annotation linkerd.io/inject must not be disabled
|
|
pods := applicationPods[applicationName]
|
|
for _, pod := range pods {
|
|
if pod.Annotations["linkerd.io/inject"] == "disabled" {
|
|
continue
|
|
}
|
|
}
|
|
|
|
if !application.Namespace.Open {
|
|
for _, applicationNameFrom := range communication.From {
|
|
applicationFrom, networkFrom, _ := config.GetApplication(applicationNameFrom)
|
|
if applicationFrom != nil && !applicationFrom.Namespace.Open {
|
|
continue
|
|
}
|
|
if networkFrom == nil && cluster.IsLinkerdEnabled(application) {
|
|
openToClosedAccess[applicationNameFrom] = applicationName
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
for appFrom, appTo := range openToClosedAccess {
|
|
LogValidationMsg(Error, "Access from 'open' application '%s' to 'closed' application '%s'. This will lead to generation of a network authentication for this workload.",
|
|
appFrom, appTo)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func podsNotPartOfAnyApplication(cluster *Cluster, namespace string, ns *Namespace) {
|
|
// Pods in the nemsapce that are not covered by any application
|
|
|
|
namespacePods := cluster.PodList(namespace)
|
|
namespacePods = slices.DeleteFunc(namespacePods, func(pod v1.Pod) bool {
|
|
return pod.Spec.HostNetwork == true
|
|
})
|
|
podNames := make(map[string]bool)
|
|
for _, pod := range namespacePods {
|
|
podNames[pod.Name] = true
|
|
}
|
|
for _, application := range ns.Applications {
|
|
for _, pod := range cluster.Pods(application) {
|
|
delete(podNames, pod.Name)
|
|
}
|
|
}
|
|
for podName, _ := range podNames {
|
|
LogValidationMsg(Error, "ERROR: pod %s/%s not part of any applications",
|
|
namespace, podName)
|
|
}
|
|
}
|
|
|
|
func HasPort(pod v1.Pod, port Port) bool {
|
|
if port.Protocol == "" {
|
|
port.Protocol = "TCP"
|
|
}
|
|
for _, container := range pod.Spec.Containers {
|
|
for _, kport := range container.Ports {
|
|
if kport.Protocol == "" {
|
|
kport.Protocol = "TCP"
|
|
}
|
|
if port.Protocol == string(kport.Protocol) && (port.Port == kport.Name || port.Port == strconv.Itoa(int(kport.ContainerPort))) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|