istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/test/framework/components/zipkin/kube.go (about)

     1  // Copyright Istio Authors
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package zipkin
    16  
    17  import (
    18  	"encoding/json"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"net"
    23  	"net/http"
    24  	"os"
    25  	"path/filepath"
    26  	"sort"
    27  	"strings"
    28  	"time"
    29  
    30  	istioKube "istio.io/istio/pkg/kube"
    31  	"istio.io/istio/pkg/test/env"
    32  	"istio.io/istio/pkg/test/framework/components/cluster"
    33  	"istio.io/istio/pkg/test/framework/components/istio"
    34  	"istio.io/istio/pkg/test/framework/resource"
    35  	testKube "istio.io/istio/pkg/test/kube"
    36  	"istio.io/istio/pkg/test/scopes"
    37  )
    38  
    39  const (
    40  	appName    = "zipkin"
    41  	tracesAPI  = "/api/v2/traces?limit=%d&spanName=%s&annotationQuery=%s"
    42  	zipkinPort = 9411
    43  
    44  	remoteZipkinEntry = `
    45  apiVersion: networking.istio.io/v1alpha3
    46  kind: Gateway
    47  metadata:
    48    name: tracing-gateway
    49    namespace: istio-system
    50  spec:
    51    selector:
    52      istio: ingressgateway
    53    servers:
    54    - port:
    55        number: 80
    56        name: http-tracing
    57        protocol: HTTP
    58      hosts:
    59      - "tracing.{INGRESS_DOMAIN}"
    60    - port:
    61        number: 9411
    62        name: http-tracing-span
    63        protocol: HTTP
    64      hosts:
    65      - "tracing.{INGRESS_DOMAIN}"
    66  ---
    67  apiVersion: networking.istio.io/v1alpha3
    68  kind: VirtualService
    69  metadata:
    70    name: tracing-vs
    71    namespace: istio-system
    72  spec:
    73    hosts:
    74    - "tracing.{INGRESS_DOMAIN}"
    75    gateways:
    76    - tracing-gateway
    77    http:
    78    - match:
    79      - port: 80
    80      route:
    81      - destination:
    82          host: tracing
    83          port:
    84            number: 80
    85    - match:
    86      - port: 9411
    87      route:
    88      - destination:
    89          host: tracing
    90          port:
    91            number: 9411
    92  ---
    93  apiVersion: networking.istio.io/v1alpha3
    94  kind: DestinationRule
    95  metadata:
    96    name: tracing
    97    namespace: istio-system
    98  spec:
    99    host: tracing
   100    trafficPolicy:
   101      tls:
   102        mode: DISABLE
   103  ---`
   104  
   105  	extServiceEntry = `
   106  apiVersion: networking.istio.io/v1alpha3
   107  kind: ServiceEntry
   108  metadata:
   109    name: zipkin
   110  spec:
   111    hosts:
   112    # must be of form name.namespace.global
   113    - zipkin.istio-system.global
   114    # Treat remote cluster services as part of the service mesh
   115    # as all clusters in the service mesh share the same root of trust.
   116    location: MESH_INTERNAL
   117    ports:
   118    - name: http-tracing-span
   119      number: 9411
   120      protocol: http
   121    resolution: DNS
   122    addresses:
   123    # the IP address to which zipkin.istio-system.global will resolve to
   124    # must be unique for each remote service, within a given cluster.
   125    # This address need not be routable. Traffic for this IP will be captured
   126    # by the sidecar and routed appropriately.
   127    - 240.0.0.2
   128    endpoints:
   129    # This is the routable address of the ingress gateway in cluster1 that
   130    # sits in front of zipkin service. Traffic from the sidecar will be
   131    # routed to this address.
   132    - address: {INGRESS_DOMAIN}
   133      ports:
   134        http-tracing-span: 15443 # Do not change this port value
   135  `
   136  )
   137  
   138  var (
   139  	_ Instance  = &kubeComponent{}
   140  	_ io.Closer = &kubeComponent{}
   141  )
   142  
   143  type kubeComponent struct {
   144  	id        resource.ID
   145  	address   string
   146  	forwarder istioKube.PortForwarder
   147  	cluster   cluster.Cluster
   148  }
   149  
   150  func getZipkinYaml() (string, error) {
   151  	yamlBytes, err := os.ReadFile(filepath.Join(env.IstioSrc, "samples/addons/extras/zipkin.yaml"))
   152  	if err != nil {
   153  		return "", err
   154  	}
   155  	yaml := string(yamlBytes)
   156  	return yaml, nil
   157  }
   158  
   159  func installZipkin(ctx resource.Context, ns string) error {
   160  	yaml, err := getZipkinYaml()
   161  	if err != nil {
   162  		return err
   163  	}
   164  	return ctx.ConfigKube().YAML(ns, yaml).Apply()
   165  }
   166  
   167  func installServiceEntry(ctx resource.Context, ns, ingressAddr string) error {
   168  	// Setup remote access to zipkin in cluster
   169  	yaml := strings.ReplaceAll(remoteZipkinEntry, "{INGRESS_DOMAIN}", ingressAddr)
   170  	err := ctx.ConfigIstio().YAML(ns, yaml).Apply()
   171  	if err != nil {
   172  		return err
   173  	}
   174  	yaml = strings.ReplaceAll(extServiceEntry, "{INGRESS_DOMAIN}", ingressAddr)
   175  	err = ctx.ConfigIstio().YAML(ns, yaml).Apply()
   176  	if err != nil {
   177  		return err
   178  	}
   179  	return nil
   180  }
   181  
   182  func newKube(ctx resource.Context, cfgIn Config) (Instance, error) {
   183  	c := &kubeComponent{
   184  		cluster: ctx.Clusters().GetOrDefault(cfgIn.Cluster),
   185  	}
   186  	c.id = ctx.TrackResource(c)
   187  
   188  	// Find the zipkin pod and service, and start forwarding a local port.
   189  	cfg, err := istio.DefaultConfig(ctx)
   190  	if err != nil {
   191  		return nil, err
   192  	}
   193  
   194  	if err := installZipkin(ctx, cfg.TelemetryNamespace); err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	fetchFn := testKube.NewSinglePodFetch(c.cluster, cfg.SystemNamespace, fmt.Sprintf("app=%s", appName))
   199  	pods, err := testKube.WaitUntilPodsAreReady(fetchFn)
   200  	if err != nil {
   201  		return nil, err
   202  	}
   203  	pod := pods[0]
   204  
   205  	forwarder, err := c.cluster.NewPortForwarder(pod.Name, pod.Namespace, "", 0, zipkinPort)
   206  	if err != nil {
   207  		return nil, err
   208  	}
   209  
   210  	if err := forwarder.Start(); err != nil {
   211  		return nil, err
   212  	}
   213  	c.forwarder = forwarder
   214  	scopes.Framework.Debugf("initialized zipkin port forwarder: %v", forwarder.Address())
   215  
   216  	isIP := net.ParseIP(cfgIn.IngressAddr).String() != "<nil>"
   217  	ingressDomain := cfgIn.IngressAddr
   218  	if isIP {
   219  		ingressDomain = fmt.Sprintf("%s.sslip.io", strings.ReplaceAll(cfgIn.IngressAddr, ":", "-"))
   220  	}
   221  
   222  	c.address = fmt.Sprintf("http://tracing.%s", ingressDomain)
   223  	scopes.Framework.Debugf("Zipkin address: %s ", c.address)
   224  	err = installServiceEntry(ctx, cfg.TelemetryNamespace, ingressDomain)
   225  	if err != nil {
   226  		return nil, err
   227  	}
   228  	return c, nil
   229  }
   230  
   231  func (c *kubeComponent) ID() resource.ID {
   232  	return c.id
   233  }
   234  
   235  func (c *kubeComponent) QueryTraces(limit int, spanName, annotationQuery string) ([]Trace, error) {
   236  	// Get 100 most recent traces
   237  	client := http.Client{
   238  		Timeout: 5 * time.Second,
   239  	}
   240  	scopes.Framework.Debugf("make get call to zipkin api %v", c.address+fmt.Sprintf(tracesAPI, limit, spanName, annotationQuery))
   241  	resp, err := client.Get(c.address + fmt.Sprintf(tracesAPI, limit, spanName, annotationQuery))
   242  	if err != nil {
   243  		scopes.Framework.Debugf("zipking err %v", err)
   244  		return nil, err
   245  	}
   246  	if resp.StatusCode != http.StatusOK {
   247  		scopes.Framework.Debugf("response err %v", resp.StatusCode)
   248  		return nil, fmt.Errorf("zipkin api returns non-ok: %v", resp.StatusCode)
   249  	}
   250  	defer resp.Body.Close()
   251  	body, err := io.ReadAll(resp.Body)
   252  	if err != nil {
   253  		return nil, err
   254  	}
   255  	traces, err := extractTraces(body)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  	return traces, nil
   260  }
   261  
   262  // Close implements io.Closer.
   263  func (c *kubeComponent) Close() error {
   264  	if c.forwarder != nil {
   265  		c.forwarder.Close()
   266  	}
   267  	return nil
   268  }
   269  
   270  func extractTraces(resp []byte) ([]Trace, error) {
   271  	var traceObjs []any
   272  	if err := json.Unmarshal(resp, &traceObjs); err != nil {
   273  		return []Trace{}, err
   274  	}
   275  	var ret []Trace
   276  	for _, t := range traceObjs {
   277  		spanObjs, ok := t.([]any)
   278  		if !ok || len(spanObjs) == 0 {
   279  			scopes.Framework.Debugf("cannot parse or cannot find spans in trace object %+v", t)
   280  			continue
   281  		}
   282  		var spans []Span
   283  		for _, obj := range spanObjs {
   284  			newSpan := buildSpan(obj)
   285  			spans = append(spans, newSpan)
   286  		}
   287  		for p := range spans {
   288  			for c := range spans {
   289  				if spans[c].ParentSpanID == spans[p].SpanID {
   290  					spans[p].ChildSpans = append(spans[p].ChildSpans, &spans[c])
   291  				}
   292  			}
   293  			// make order of child spans deterministic
   294  			sort.Slice(spans[p].ChildSpans, func(i, j int) bool {
   295  				return spans[p].ChildSpans[i].Name < spans[p].ChildSpans[j].Name
   296  			})
   297  		}
   298  		ret = append(ret, Trace{Spans: spans})
   299  	}
   300  	if len(ret) > 0 {
   301  		return ret, nil
   302  	}
   303  	return []Trace{}, errors.New("cannot find any traces")
   304  }
   305  
   306  func buildSpan(obj any) Span {
   307  	var s Span
   308  	spanSpec := obj.(map[string]any)
   309  	if spanID, ok := spanSpec["id"]; ok {
   310  		s.SpanID = spanID.(string)
   311  	}
   312  	if parentSpanID, ok := spanSpec["parentId"]; ok {
   313  		s.ParentSpanID = parentSpanID.(string)
   314  	}
   315  	if endpointObj, ok := spanSpec["localEndpoint"]; ok {
   316  		if em, ok := endpointObj.(map[string]any); ok {
   317  			s.ServiceName = em["serviceName"].(string)
   318  		}
   319  	}
   320  	if name, ok := spanSpec["name"]; ok {
   321  		s.Name = name.(string)
   322  	}
   323  	return s
   324  }