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 }