Skip to content

Commit

Permalink
'istioctl describe' support no-mixer config and name RBAC policies (i…
Browse files Browse the repository at this point in the history
…stio#16488)

* Find VirtualService if mixer config not present in Envoy dump

* Show RBAC policy incoming to pod

* Tests and data for istioctl describe
  • Loading branch information
esnible authored and istio-testing committed Aug 27, 2019
1 parent 2d04623 commit 5107a90
Show file tree
Hide file tree
Showing 3 changed files with 4,906 additions and 3 deletions.
91 changes: 89 additions & 2 deletions istioctl/cmd/describe.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (

envoy_api_core "github.com/envoyproxy/go-control-plane/envoy/api/v2/core"
envoy_api_route "github.com/envoyproxy/go-control-plane/envoy/api/v2/route"
rbac_http_filter "github.com/envoyproxy/go-control-plane/envoy/config/filter/http/rbac/v2"
http_conn "github.com/envoyproxy/go-control-plane/envoy/config/filter/network/http_connection_manager/v2"
gogo_types "github.com/gogo/protobuf/types"
"github.com/spf13/cobra"

Expand All @@ -39,6 +41,7 @@ import (
istio_envoy_configdump "istio.io/istio/istioctl/pkg/writer/envoy/configdump"
"istio.io/istio/pilot/pkg/model"
envoy_v2 "istio.io/istio/pilot/pkg/proxy/envoy/v2"
authz_model "istio.io/istio/pilot/pkg/security/authz/model"
pilotcontroller "istio.io/istio/pilot/pkg/serviceregistry/kube/controller"
"istio.io/istio/pkg/config/host"
"istio.io/istio/pkg/config/protocol"
Expand Down Expand Up @@ -189,6 +192,16 @@ THIS COMMAND IS STILL UNDER ACTIVE DEVELOPMENT AND NOT READY FOR PRODUCTION USE.
printVirtualService(writer, *vs, svc, matchingSubsets, nonmatchingSubsets, dr)
}
}

policies, _ := getIstioRBACPolicies(&cd, port.Port)
if len(policies) > 0 {
if len(svc.Spec.Ports) > 1 {
// If there is more than one port, prefix each DR by the port it applies to
fmt.Fprintf(writer, "%d ", port.Port)
}

fmt.Fprintf(writer, "RBAC policies: %s\n", strings.Join(policies, ", "))
}
}
}

Expand Down Expand Up @@ -671,6 +684,64 @@ func (v *myGogoValue) keyAsString(key string) string {
return s.GetStringValue()
}

func getIstioRBACPolicies(cd *configdump.Wrapper, port int32) ([]string, error) {
hcm, err := getInboundHTTPConnectionManager(cd, port)
if err != nil || hcm == nil {
return []string{}, err
}

// Identify RBAC policies. Currently there are no "breadcrumbs" so we only
// return the policy names, not the ServiceRole and ServiceRoleBinding names.
for _, httpFilter := range hcm.HttpFilters {
if httpFilter.Name == authz_model.RBACHTTPFilterName {
rbac := &rbac_http_filter.RBAC{}
if err := gogo_types.UnmarshalAny(httpFilter.GetTypedConfig(), rbac); err == nil {
policies := []string{}
for polName := range rbac.Rules.Policies {
policies = append(policies, polName)
}
return policies, nil
}
}
}

return []string{}, nil
}

// Return the first HTTP Connection Manager config for the inbound port
func getInboundHTTPConnectionManager(cd *configdump.Wrapper, port int32) (*http_conn.HttpConnectionManager, error) {
filter := istio_envoy_configdump.ListenerFilter{
Port: uint32(port),
}
listeners, err := cd.GetListenerConfigDump()
if err != nil {
return nil, err
}

for _, listener := range listeners.DynamicActiveListeners {
if filter.Verify(listener.Listener) {
sockAddr := listener.Listener.Address.GetSocketAddress()
if sockAddr != nil {
// Skip outbound listeners
if sockAddr.Address == "0.0.0.0" {
continue
}
}

for _, filterChain := range listener.Listener.FilterChains {
for _, filter := range filterChain.Filters {
hcm := &http_conn.HttpConnectionManager{}
if err := gogo_types.UnmarshalAny(filter.GetTypedConfig(), hcm); err == nil {
return hcm, nil
}
}
}
}
}

return nil, nil
}

// getIstioConfigNameForSvc returns name, namespace
func getIstioVirtualServiceNameForSvc(cd *configdump.Wrapper, svc v1.Service, port int32) (string, string, error) {
path, err := getIstioVirtualServicePathForSvcFromRoute(cd, svc, port)
Expand Down Expand Up @@ -709,7 +780,7 @@ func getIstioVirtualServicePathForSvcFromRoute(cd *configdump.Wrapper, svc v1.Se

for _, vh := range rcd.RouteConfig.VirtualHosts {
for _, route := range vh.Routes {
if routeDestinationMatchesSvc(route, svc) {
if routeDestinationMatchesSvc(route, svc, vh) {
return getIstioConfig(route.Metadata)
}
}
Expand All @@ -719,7 +790,7 @@ func getIstioVirtualServicePathForSvcFromRoute(cd *configdump.Wrapper, svc v1.Se
}

// routeDestinationMatchesSvc determines if there ismixer configuration to use this service as a destination
func routeDestinationMatchesSvc(route *envoy_api_route.Route, svc v1.Service) bool {
func routeDestinationMatchesSvc(route *envoy_api_route.Route, svc v1.Service, vh *envoy_api_route.VirtualHost) bool {
if route == nil {
return false
}
Expand All @@ -746,6 +817,17 @@ func routeDestinationMatchesSvc(route *envoy_api_route.Route, svc v1.Service) bo
}
}

// No mixer config, infer from VirtualHost domains matching <service>.<namespace>.svc.cluster.local
re := regexp.MustCompile(`(?P<service>[^\.]+)\.(?P<namespace>[^\.]+)\.svc\.cluster\.local$`)
for _, domain := range vh.Domains {
ss := re.FindStringSubmatch(domain)
if ss != nil {
if ss[1] == svc.ObjectMeta.Name && ss[2] == svc.ObjectMeta.Namespace {
return true
}
}
}

return false
}

Expand Down Expand Up @@ -911,6 +993,7 @@ func printAuthnFromAuthenticationz(writer io.Writer, debug *[]envoy_v2.Authentic
return
}

count := 0
matchingAuthns := []envoy_v2.AuthenticationDebug{}
for _, authn := range *debug {
if authnMatchSvc(authn, svc, port) {
Expand All @@ -919,8 +1002,12 @@ func printAuthnFromAuthenticationz(writer io.Writer, debug *[]envoy_v2.Authentic
}
for _, matchingAuthn := range matchingAuthns {
printAuthn(writer, pod, matchingAuthn)
count++
}

if count == 0 {
fmt.Fprintf(writer, "None\n")
}
}

// getIstioVirtualServicePathForSvcFromListener returns something like "/apis/networking/v1alpha3/namespaces/default/virtual-service/reviews"
Expand Down
116 changes: 115 additions & 1 deletion istioctl/cmd/describe_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,15 @@ import (
coreV1 "k8s.io/api/core/v1"
metaV1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/fake"

networking "istio.io/api/networking/v1alpha3"

"istio.io/istio/pilot/pkg/model"
"istio.io/istio/pilot/test/util"
"istio.io/istio/pkg/config/schemas"
)

// execAndK8sConfigTestCase lets a test case hold some Envoy, Istio, and Kubernetes configuration
Expand All @@ -47,7 +51,33 @@ type execAndK8sConfigTestCase struct {
}

var (
cannedIstioConfig = []model.Config{}
cannedIstioConfig = []model.Config{
{
ConfigMeta: model.ConfigMeta{
Name: "ratings",
Namespace: "bookinfo",
Type: schemas.DestinationRule.Type,
Group: schemas.DestinationRule.Group,
Version: schemas.DestinationRule.Version,
},
Spec: &networking.DestinationRule{
Host: "ratings",
Subsets: []*networking.Subset{
{
Name: "v1",
Labels: map[string]string{
"version": "v1",
},
},
},
TrafficPolicy: &networking.TrafficPolicy{
Tls: &networking.TLSSettings{
Mode: networking.TLSSettings_ISTIO_MUTUAL,
},
},
},
},
}

cannedK8sEnv = []runtime.Object{
&coreV1.PodList{Items: []coreV1.Pod{
Expand Down Expand Up @@ -78,6 +108,43 @@ var (
Phase: coreV1.PodRunning,
},
},
{
ObjectMeta: metaV1.ObjectMeta{
Name: "ratings-v1-f745cf57b-vfwcv",
Namespace: "bookinfo",
Labels: map[string]string{
"app": "ratings",
"version": "v1",
},
},
Spec: coreV1.PodSpec{
NodeName: "foo_node",
Containers: []coreV1.Container{
{
Name: "ratings",
Ports: []coreV1.ContainerPort{
{
ContainerPort: 9080,
Protocol: "TCP",
},
},
},
{
Name: "istio-proxy",
Ports: []coreV1.ContainerPort{
{
Name: "http-envoy-prom",
ContainerPort: 15090,
Protocol: "TCP",
},
},
},
},
},
Status: coreV1.PodStatus{
Phase: coreV1.PodRunning,
},
},
}},
&coreV1.ServiceList{Items: []coreV1.Service{
{
Expand All @@ -95,13 +162,34 @@ var (
Selector: map[string]string{"app": "details"},
},
},
{
ObjectMeta: metaV1.ObjectMeta{
Name: "ratings",
Namespace: "bookinfo",
},
Spec: coreV1.ServiceSpec{
Ports: []coreV1.ServicePort{
{
Port: 9080,
TargetPort: intstr.IntOrString{
Type: intstr.Int,
IntVal: 9080,
},
Name: "http",
Protocol: "TCP",
},
},
Selector: map[string]string{"app": "ratings"},
},
},
}},
}
)

func TestDescribe(t *testing.T) {
cannedConfig := map[string][]byte{
"details-v1-5b7f94f9bc-wp5tb": util.ReadFile("../pkg/writer/compare/testdata/envoyconfigdump.json", t),
"ratings-v1-f745cf57b-vfwcv": util.ReadFile("testdata/describe/ratings-v1-f745cf57b-vfwcv.json", t),
"istio-pilot-7f9796fc98-99bp7": []byte(`[
{
"host": "details.default.svc.cluster.local",
Expand All @@ -111,6 +199,15 @@ func TestDescribe(t *testing.T) {
"server_protocol": "HTTP/mTLS",
"client_protocol": "HTTP",
"TLS_conflict_status": "OK"
},
{
"host": "ratings.bookinfo.svc.cluster.local",
"port": 9080,
"authentication_policy_name": "default/",
"destination_rule_name": "details/default",
"server_protocol": "HTTP/mTLS",
"client_protocol": "mTLS",
"TLS_conflict_status": "OK"
}
]`),
}
Expand Down Expand Up @@ -144,6 +241,23 @@ Suggestion: add 'version' label to pod for Istio telemetry.
--------------------
Service: details
Pilot reports that pod is PERMISSIVE (enforces HTTP/mTLS) and clients speak HTTP
`,
},
{ // case 5 has recent data including RBAC
execClientConfig: cannedConfig,
configs: cannedIstioConfig,
k8sConfigs: cannedK8sEnv,
args: strings.Split("-n bookinfo experimental describe pod ratings-v1-f745cf57b-vfwcv", " "),
expectedOutput: `Pod: ratings-v1-f745cf57b-vfwcv
Pod Ports: 9080 (ratings), 15090 (istio-proxy)
--------------------
Service: ratings
Port: http 9080/HTTP
DestinationRule: ratings for "ratings"
Matching subsets: v1
Traffic Policy TLS Mode: ISTIO_MUTUAL
Pilot reports that pod is PERMISSIVE (enforces HTTP/mTLS) and clients speak mTLS
RBAC policies: ratings-reader
`,
},
}
Expand Down
Loading

0 comments on commit 5107a90

Please sign in to comment.