Skip to content

Commit

Permalink
Added cross-namespace validation support for Sidecar's egress listene…
Browse files Browse the repository at this point in the history
…r host, considering exported ServiceEntries.
  • Loading branch information
hhovsepy committed Sep 22, 2021
1 parent 1826052 commit 3944805
Show file tree
Hide file tree
Showing 5 changed files with 143 additions and 38 deletions.
3 changes: 2 additions & 1 deletion business/checkers/destination_rules_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type DestinationRulesChecker struct {
ExportedDestinationRules []kubernetes.IstioObject
MTLSDetails kubernetes.MTLSDetails
ServiceEntries []kubernetes.IstioObject
ExportedServiceEntries []kubernetes.IstioObject
Namespaces []models.Namespace
}

Expand All @@ -29,7 +30,7 @@ func (in DestinationRulesChecker) Check() models.IstioValidations {
func (in DestinationRulesChecker) runGroupChecks() models.IstioValidations {
validations := models.IstioValidations{}

seHosts := kubernetes.ServiceEntryHostnames(in.ServiceEntries)
seHosts := kubernetes.ServiceEntryHostnames(append(in.ServiceEntries, in.ExportedServiceEntries...))

enabledDRCheckers := []GroupChecker{
destinationrules.MultiMatchChecker{Namespaces: in.Namespaces, DestinationRules: in.DestinationRules, ServiceEntries: seHosts, ExportedDestinationRules: in.ExportedDestinationRules},
Expand Down
36 changes: 11 additions & 25 deletions business/checkers/sidecars/egress_listener_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (

core_v1 "k8s.io/api/core/v1"

"github.com/kiali/kiali/config"
"github.com/kiali/kiali/kubernetes"
"github.com/kiali/kiali/models"
)
Expand Down Expand Up @@ -86,7 +85,6 @@ func (elc EgressHostChecker) getHosts() ([]HostWithIndex, bool) {

func (elc EgressHostChecker) validateHost(host string, egrIdx, hostIdx int) ([]*models.IstioCheck, bool) {
checks := make([]*models.IstioCheck, 0)
ins := config.Get().IstioNamespace
sns := elc.Sidecar.GetObjectMeta().Namespace

hostNs, dnsName, valid := getHostComponents(host)
Expand All @@ -99,44 +97,32 @@ func (elc EgressHostChecker) validateHost(host string, egrIdx, hostIdx int) ([]*
return checks, true
}

// Show cross-namespace validation
// when namespace is different to both istio control plane or sidecar namespace
if hostNs != ins && hostNs != sns && hostNs != "." {
return append(checks, buildCheck("validation.unable.cross-namespace", egrIdx, hostIdx)), true
// namespace/* is a valid scenario
if dnsName == "*" {
return checks, true
}

// Lookup services when ns is . or sidecar namespace
if hostNs == sns || hostNs == "." {
// namespace/* is a valid scenario
if dnsName == "*" {
return checks, true
}

// Parse the dnsName to a kubernetes Host
fqdn := kubernetes.ParseHost(dnsName, sns, elc.Sidecar.GetObjectMeta().ClusterName)
if fqdn.Namespace != sns && fqdn.Namespace != "" {
return append(checks, buildCheck("validation.unable.cross-namespace", egrIdx, hostIdx)), true
}
// Parse the dnsName to a kubernetes Host
fqdn := kubernetes.ParseHost(dnsName, sns, elc.Sidecar.GetObjectMeta().ClusterName)

// Lookup for matching services
if !elc.HasMatchingService(fqdn, sns) {
checks = append(checks, buildCheck("sidecar.egress.servicenotfound", egrIdx, hostIdx))
}
// Lookup for matching services
if !elc.HasMatchingService(fqdn, sns) {
checks = append(checks, buildCheck("sidecar.egress.servicenotfound", egrIdx, hostIdx))
}

return checks, true
}

func (elc EgressHostChecker) HasMatchingService(host kubernetes.Host, itemNamespace string) bool {
if strings.HasPrefix(host.Service, "*") {
// Check wildcard hosts - needs to match "*" and "*.suffix" also.
if host.IsWildcard() && host.Namespace == itemNamespace {
return true
}

if kubernetes.HasMatchingServices(host.Service, elc.Services) {
return true
}

return kubernetes.HasMatchingServiceEntries(host.Service, elc.ServiceEntries)
return kubernetes.HasMatchingServiceEntries(host.String(), elc.ServiceEntries)
}

func getHostComponents(host string) (string, string, bool) {
Expand Down
125 changes: 121 additions & 4 deletions business/checkers/sidecars/egress_listener_checker_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func TestEgressHostFormatCorrect(t *testing.T) {
"~/*",
"./*",
"./reviews.bookinfo.svc.cluster.local",
"./*.bookinfo.svc.cluster.com",
"./*.bookinfo.svc.cluster.local",
"./wikipedia.org",
"bookinfo/*",
"bookinfo/*.bookinfo.svc.cluster.local",
Expand All @@ -38,7 +38,124 @@ func TestEgressHostFormatCorrect(t *testing.T) {
assert.True(valid)
}

func TestEgressHostCrossNamespace(t *testing.T) {
func TestEgressExportedInternalServiceEntryPresent(t *testing.T) {
assert := assert.New(t)

vals, valid := EgressHostChecker{
Services: []core_v1.Service{},
ServiceEntries: kubernetes.ServiceEntryHostnames([]kubernetes.IstioObject{data.CreateEmptyMeshInternalServiceEntry("details-se", "bookinfo3", []string{"details.bookinfo2.svc.cluster.local"})}),
Sidecar: sidecarWithHosts([]interface{}{
"bookinfo/details.bookinfo2.svc.cluster.local",
}),
}.Check()

assert.Empty(vals)
assert.True(valid)
}

func TestEgressExportedExternalServiceEntryPresent(t *testing.T) {
assert := assert.New(t)

vals, valid := EgressHostChecker{
Services: []core_v1.Service{},
ServiceEntries: kubernetes.ServiceEntryHostnames([]kubernetes.IstioObject{data.CreateEmptyMeshExternalServiceEntry("details-se", "bookinfo3", []string{"www.myhost.com"})}),
Sidecar: sidecarWithHosts([]interface{}{
"bookinfo/www.myhost.com",
}),
}.Check()

assert.Empty(vals)
assert.True(valid)
}

func TestEgressExportedInternalServiceEntryNotPresent(t *testing.T) {
assert := assert.New(t)

vals, valid := EgressHostChecker{
Services: []core_v1.Service{},
ServiceEntries: kubernetes.ServiceEntryHostnames([]kubernetes.IstioObject{data.CreateEmptyMeshInternalServiceEntry("details-se", "bookinfo3", []string{"details.bookinfo2.svc.cluster.local"})}),
Sidecar: sidecarWithHosts([]interface{}{
"bookinfo/details.bookinfo.svc.cluster.local",
}),
}.Check()

assert.NotEmpty(vals)
assert.True(valid)
assert.Equal(models.WarningSeverity, vals[0].Severity)
assert.Equal("spec/egress[0]/hosts[0]", vals[0].Path)
assert.NoError(validations.ConfirmIstioCheckMessage("sidecar.egress.servicenotfound", vals[0]))
}

func TestEgressExportedExternalServiceEntryNotPresent(t *testing.T) {
assert := assert.New(t)

vals, valid := EgressHostChecker{
Services: []core_v1.Service{},
ServiceEntries: kubernetes.ServiceEntryHostnames([]kubernetes.IstioObject{data.CreateEmptyMeshExternalServiceEntry("details-se", "bookinfo3", []string{"www.myhost.com"})}),
Sidecar: sidecarWithHosts([]interface{}{
"bookinfo/www.wrong.com",
}),
}.Check()

assert.NotEmpty(vals)
assert.True(valid)
assert.Equal(models.WarningSeverity, vals[0].Severity)
assert.Equal("spec/egress[0]/hosts[0]", vals[0].Path)
assert.NoError(validations.ConfirmIstioCheckMessage("sidecar.egress.servicenotfound", vals[0]))
}

func TestEgressExportedWildcardInternalServiceEntryPresent(t *testing.T) {
assert := assert.New(t)

vals, valid := EgressHostChecker{
Services: []core_v1.Service{},
ServiceEntries: kubernetes.ServiceEntryHostnames([]kubernetes.IstioObject{data.CreateEmptyMeshInternalServiceEntry("details-se", "bookinfo3", []string{"*.bookinfo2.svc.cluster.local"})}),
Sidecar: sidecarWithHosts([]interface{}{
"bookinfo/details.bookinfo2.svc.cluster.local",
}),
}.Check()

assert.Empty(vals)
assert.True(valid)
}

func TestEgressExportedWildcardInternalServiceEntryNotPresent(t *testing.T) {
assert := assert.New(t)

vals, valid := EgressHostChecker{
Services: []core_v1.Service{},
ServiceEntries: kubernetes.ServiceEntryHostnames([]kubernetes.IstioObject{data.CreateEmptyMeshInternalServiceEntry("details-se", "bookinfo3", []string{"*.bookinfo3.svc.cluster.local"})}),
Sidecar: sidecarWithHosts([]interface{}{
"bookinfo/*.bookinfo2.svc.cluster.local",
}),
}.Check()

assert.NotEmpty(vals)
assert.True(valid)
assert.Equal(models.WarningSeverity, vals[0].Severity)
assert.Equal("spec/egress[0]/hosts[0]", vals[0].Path)
assert.NoError(validations.ConfirmIstioCheckMessage("sidecar.egress.servicenotfound", vals[0]))
}

func TestEgressExportedNonFQDNInternalServiceEntryNotPresent(t *testing.T) {
assert := assert.New(t)

vals, valid := EgressHostChecker{
Services: []core_v1.Service{},
ServiceEntries: kubernetes.ServiceEntryHostnames([]kubernetes.IstioObject{data.CreateEmptyMeshInternalServiceEntry("details-se", "bookinfo3", []string{"details"})}),
Sidecar: sidecarWithHosts([]interface{}{
"bookinfo/details.bookinfo2.svc.cluster.local",
}),
}.Check()

assert.NotEmpty(vals)
assert.True(valid)
assert.Equal(models.WarningSeverity, vals[0].Severity)
assert.Equal("spec/egress[0]/hosts[0]", vals[0].Path)
assert.NoError(validations.ConfirmIstioCheckMessage("sidecar.egress.servicenotfound", vals[0]))
}

func TestEgressHostCrossNamespaceServiceNotFound(t *testing.T) {
assert := assert.New(t)

hosts := []interface{}{
Expand All @@ -61,9 +178,9 @@ func TestEgressHostCrossNamespace(t *testing.T) {
assert.True(valid)

for i, c := range vals {
assert.Equal(models.Unknown, c.Severity)
assert.Equal(models.WarningSeverity, c.Severity)
assert.Equal(fmt.Sprintf("spec/egress[0]/hosts[%d]", i), c.Path)
assert.NoError(validations.ConfirmIstioCheckMessage("validation.unable.cross-namespace", c))
assert.NoError(validations.ConfirmIstioCheckMessage("sidecar.egress.servicenotfound", c))
}
}

Expand Down
13 changes: 7 additions & 6 deletions business/checkers/sidecars_checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import (
const SidecarCheckerType = "sidecar"

type SidecarChecker struct {
Sidecars []kubernetes.IstioObject
ServiceEntries []kubernetes.IstioObject
Services []core_v1.Service
Namespaces models.Namespaces
WorkloadList models.WorkloadList
Sidecars []kubernetes.IstioObject
ServiceEntries []kubernetes.IstioObject
ExportedServiceEntries []kubernetes.IstioObject
Services []core_v1.Service
Namespaces models.Namespaces
WorkloadList models.WorkloadList
}

func (s SidecarChecker) Check() models.IstioValidations {
Expand Down Expand Up @@ -55,7 +56,7 @@ func (s SidecarChecker) runIndividualChecks() models.IstioValidations {
func (s SidecarChecker) runChecks(sidecar kubernetes.IstioObject) models.IstioValidations {
policyName := sidecar.GetObjectMeta().Name
key, rrValidation := EmptyValidValidation(policyName, sidecar.GetObjectMeta().Namespace, SidecarCheckerType)
serviceHosts := kubernetes.ServiceEntryHostnames(s.ServiceEntries)
serviceHosts := kubernetes.ServiceEntryHostnames(append(s.ServiceEntries, s.ExportedServiceEntries...))

enabledCheckers := []Checker{
common.WorkloadSelectorNoWorkloadFoundChecker(SidecarCheckerType, sidecar, s.WorkloadList),
Expand Down
4 changes: 2 additions & 2 deletions business/istio_validations.go
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ func (in *IstioValidationsService) getAllObjectCheckers(namespace string, istioD
checkers.PeerAuthenticationChecker{PeerAuthentications: mtlsDetails.PeerAuthentications, MTLSDetails: mtlsDetails, WorkloadList: workloads},
checkers.ServiceEntryChecker{ServiceEntries: istioDetails.ServiceEntries, Namespaces: namespaces},
checkers.AuthorizationPolicyChecker{AuthorizationPolicies: rbacDetails.AuthorizationPolicies, Namespace: namespace, Namespaces: namespaces, Services: services, ServiceEntries: istioDetails.ServiceEntries, WorkloadList: workloads, MtlsDetails: mtlsDetails, VirtualServices: istioDetails.VirtualServices, RegistryStatus: registryStatus},
checkers.SidecarChecker{Sidecars: istioDetails.Sidecars, Namespaces: namespaces, WorkloadList: workloads, Services: services, ServiceEntries: istioDetails.ServiceEntries},
checkers.SidecarChecker{Sidecars: istioDetails.Sidecars, Namespaces: namespaces, WorkloadList: workloads, Services: services, ServiceEntries: istioDetails.ServiceEntries, ExportedServiceEntries: exportedResources.ServiceEntries},
checkers.RequestAuthenticationChecker{RequestAuthentications: istioDetails.RequestAuthentications, WorkloadList: workloads},
}
}
Expand Down Expand Up @@ -190,7 +190,7 @@ func (in *IstioValidationsService) GetIstioObjectValidations(namespace string, o
objectCheckers = []ObjectChecker{serviceEntryChecker}
case kubernetes.Sidecars:
sidecarsChecker := checkers.SidecarChecker{Sidecars: istioDetails.Sidecars, Namespaces: namespaces,
WorkloadList: workloads, Services: services, ServiceEntries: istioDetails.ServiceEntries}
WorkloadList: workloads, Services: services, ServiceEntries: istioDetails.ServiceEntries, ExportedServiceEntries: exportedResources.ServiceEntries}
objectCheckers = []ObjectChecker{sidecarsChecker}
case kubernetes.AuthorizationPolicies:
authPoliciesChecker := checkers.AuthorizationPolicyChecker{AuthorizationPolicies: rbacDetails.AuthorizationPolicies,
Expand Down

0 comments on commit 3944805

Please sign in to comment.