...

Text file src/github.com/emissary-ingress/emissary/v3/python/ambassador/diagnostics/diagnostics.py

Documentation: github.com/emissary-ingress/emissary/v3/python/ambassador/diagnostics

     1# Copyright 2018 Datawire. All rights reserved.
     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
    15import logging
    16import re
    17from typing import Any, Dict, List, Optional, Tuple
    18from typing import cast as typecast
    19
    20from ..envoy import EnvoyConfig
    21from ..ir import IR
    22from ..ir.irbasemappinggroup import IRBaseMappingGroup
    23from ..ir.irhttpmappinggroup import IRHTTPMappingGroup
    24from ..utils import dump_json
    25from .envoy_stats import EnvoyStats
    26
    27
    28class DiagSource(dict):
    29    pass
    30
    31
    32class DiagCluster(dict):
    33    """
    34    A DiagCluster represents what Envoy thinks about the health of a cluster.
    35    DO NOT JUST PASS AN IRCluster into DiagCluster; turn it into a dict with
    36    .as_dict() first.
    37    """
    38
    39    def __init__(self, cluster) -> None:
    40        super().__init__(**cluster)
    41
    42    def update_health(self, other: dict) -> None:
    43        for from_key, to_key in [
    44            ("health", "_health"),
    45            ("hmetric", "_hmetric"),
    46            ("hcolor", "_hcolor"),
    47        ]:
    48            if from_key in other:
    49                self[to_key] = other[from_key]
    50
    51    def default_missing(self) -> dict:
    52        for key, default in [
    53            ("service", "unknown service!"),
    54            ("weight", 100),
    55            ("_hmetric", "unknown"),
    56            ("_hcolor", "orange"),
    57        ]:
    58            if not self.get(key, None):
    59                self[key] = default
    60
    61        return dict(self)
    62
    63    @classmethod
    64    def unknown_cluster(cls):
    65        return DiagCluster(
    66            {
    67                "service": "unknown service!",
    68                "_health": "unknown cluster!",
    69                "_hmetric": "unknown",
    70                "_hcolor": "orange",
    71            }
    72        )
    73
    74
    75class DiagClusters:
    76    """
    77    DiagClusters is, unsuprisingly, a set of DiagCluster. The thing about DiagClusters
    78    is that the [] operator always gives you a valid DiagCluster -- it'll use DiagCluster.unknown()
    79    to make a new DiagCluster if you ask for one that doesn't exist.
    80    """
    81
    82    clusters: Dict[str, DiagCluster]
    83
    84    def __init__(self, clusters: Optional[List[dict]] = None) -> None:
    85        self.clusters = {}
    86
    87        if clusters:
    88            for cluster in typecast(List[dict], clusters):
    89                self[cluster["name"]] = DiagCluster(cluster)
    90
    91    def __getitem__(self, key: str) -> DiagCluster:
    92        if key not in self.clusters:
    93            self.clusters[key] = DiagCluster.unknown_cluster()
    94
    95        return self.clusters[key]
    96
    97    def __setitem__(self, key: str, value: DiagCluster) -> None:
    98        self.clusters[key] = value
    99
   100    def __contains__(self, key: str) -> bool:
   101        return key in self.clusters
   102
   103    def as_json(self):
   104        return dump_json(self.clusters, pretty=True)
   105
   106
   107class DiagResult:
   108    """
   109    A DiagResult is the result of a diagnostics request, whether for an
   110    overview or for a particular key.
   111    """
   112
   113    def __init__(self, diag: "Diagnostics", estat: EnvoyStats, request) -> None:
   114        self.diag = diag
   115        self.logger = self.diag.logger
   116        self.estat = estat
   117
   118        # Go ahead and grab Envoy cluster stats for all possible clusters.
   119        # XXX This might be a bit silly.
   120        self.cstats = {
   121            cluster.name: self.estat.cluster_stats(cluster.stats_name)
   122            for cluster in self.diag.clusters.values()
   123        }
   124
   125        # Save the request host and scheme. We'll need them later.
   126        self.request_host = request.headers.get("Host", "*")
   127        self.request_scheme = request.headers.get("X-Forwarded-Proto", "http").lower()
   128
   129        # All of these things reflect _only_ resources that are relevant to the request
   130        # we're handling -- e.g. if you ask for a particular group, you'll only get the
   131        # clusters that are part of that group.
   132        self.clusters: Dict[str, DiagCluster] = {}  # Envoy clusters
   133        self.routes: List[dict] = []  # Envoy routes
   134        self.element_keys: Dict[str, bool] = {}  # Active element keys
   135        self.ambassador_resources: Dict[
   136            str, str
   137        ] = {}  # Actually serializations of Ambassador config resources
   138        self.envoy_resources: Dict[str, dict] = {}  # Envoy config resources
   139
   140    def as_dict(self) -> Dict[str, Any]:
   141        return {
   142            "cluster_stats": self.cstats,
   143            "cluster_info": self.clusters,
   144            "route_info": self.routes,
   145            "active_elements": sorted(self.element_keys.keys()),
   146            "ambassador_resources": self.ambassador_resources,
   147            "envoy_resources": self.envoy_resources,
   148        }
   149
   150    def include_element(self, key: str) -> None:
   151        """
   152        Note that a particular key is something relevant to this result -- e.g.
   153        'oh, the key foo-mapping.1 is active here'.
   154
   155        One problem here is that we don't currently cycle over to make sure that
   156        all the requisite higher-level objects are brought in when we mark an
   157        element active. This needs fixing.
   158
   159        :param key: the key we want to remember as being active.
   160        """
   161        self.element_keys[key] = True
   162
   163    def include_referenced_elements(self, obj: dict) -> None:
   164        """
   165        Include all of the elements in the given object's _referenced_by
   166        array.
   167
   168        :param obj: object for which to include referencing keys
   169        """
   170
   171        for element_key in obj["_referenced_by"]:
   172            self.include_element(element_key)
   173
   174    def include_cluster(self, cluster: dict) -> DiagCluster:
   175        """
   176        Note that a particular cluster and everything that references it are
   177        relevant to this result. If the cluster has related health information in
   178        our cstats, fold that in too.
   179
   180        Don't pass an IRCluster here -- turn it into a dict with as_dict()
   181        first.
   182
   183        Returns the DiagCluster that we actually use to hold everything.
   184
   185        :param cluster: dictionary version of a cluster to mark as active.
   186        :return: the DiagCluster for this cluster
   187        """
   188
   189        c_name = cluster["name"]
   190
   191        if c_name not in self.clusters:
   192            self.clusters[c_name] = DiagCluster(cluster)
   193
   194        if c_name in self.cstats:
   195            self.clusters[c_name].update_health(self.cstats[c_name])
   196
   197        self.include_referenced_elements(cluster)
   198
   199        return self.clusters[c_name]
   200
   201    def include_httpgroup(self, group: IRHTTPMappingGroup) -> None:
   202        """
   203        Note that a particular IRHTTPMappingGroup, all of the clusters it uses for upstream
   204        traffic, and everything that references it are relevant to this result.
   205
   206        This method actually does a fair amount of work around handling clusters, shadow
   207        clusters, and host_redirects. It would be a horrible mistake to duplicate this
   208        elsewhere.
   209
   210        :param group: IRHTTPMappingGroup to include
   211        """
   212
   213        # self.logger.debug("GROUP %s" % group.as_json())
   214
   215        prefix = group["prefix"] if "prefix" in group else group["regex"]
   216        rewrite = group.get("rewrite", "/")
   217        method = "*"
   218        host = None
   219
   220        route_clusters: List[DiagCluster] = []
   221
   222        for mapping in group.get("mappings", []):
   223            cluster = mapping["cluster"]
   224
   225            mapping_cluster = self.include_cluster(cluster.as_dict())
   226            mapping_cluster.update({"weight": mapping.get("weight", 100)})
   227
   228            # self.logger.debug("GROUP %s CLUSTER %s %d%% (%s)" %
   229            #                   (group['group_id'], c_name, mapping['weight'], mapping_cluster))
   230
   231            route_clusters.append(mapping_cluster)
   232
   233        host_redir = group.get("host_redirect", None)
   234
   235        if host_redir:
   236            # XXX Stupid hackery here. redirect_cluster should be a real
   237            # IRCluster object.
   238            redirect_cluster = self.include_cluster(
   239                {
   240                    "name": host_redir["name"],
   241                    "service": host_redir["service"],
   242                    "weight": 100,
   243                    "type_label": "redirect",
   244                    "_referenced_by": [host_redir["rkey"]],
   245                }
   246            )
   247
   248            route_clusters.append(redirect_cluster)
   249
   250            self.logger.debug("host_redirect route: %s" % group)
   251            self.logger.debug("host_redirect cluster: %s" % redirect_cluster)
   252
   253        shadows = group.get("shadows", [])
   254
   255        for shadow in shadows:
   256            # Shadows have a real cluster object.
   257            shadow_dict = shadow["cluster"].as_dict()
   258            shadow_dict["type_label"] = "shadow"
   259
   260            shadow_cluster = self.include_cluster(shadow_dict)
   261            route_clusters.append(shadow_cluster)
   262
   263            self.logger.debug("shadow route: %s" % group)
   264            self.logger.debug("shadow cluster: %s" % shadow_cluster)
   265
   266        headers = []
   267
   268        for header in group.get("headers", []):
   269            hdr_name = header.get("name", None)
   270            hdr_value = header.get("value", None)
   271
   272            if hdr_name == ":authority":
   273                host = hdr_value
   274            elif hdr_name == ":method":
   275                method = hdr_value
   276            else:
   277                headers.append(header)
   278
   279        sep = "" if prefix.startswith("/") else "/"
   280        route_key = "%s://%s%s%s" % (
   281            self.request_scheme,
   282            host if host else self.request_host,
   283            sep,
   284            prefix,
   285        )
   286
   287        route_info = {
   288            "_route": group.as_dict(),
   289            "_source": group["location"],
   290            "_group_id": group["group_id"],
   291            "key": route_key,
   292            "prefix": prefix,
   293            "rewrite": rewrite,
   294            "method": method,
   295            "headers": headers,
   296            "clusters": [x.default_missing() for x in route_clusters],
   297            "host": host if host else "*",
   298        }
   299
   300        if "precedence" in group:
   301            route_info["precedence"] = group["precedence"]
   302
   303        metadata_labels = group.get("metadata_labels") or {}
   304        diag_class = metadata_labels.get("ambassador_diag_class") or None
   305
   306        if diag_class:
   307            route_info["diag_class"] = diag_class
   308
   309        self.routes.append(route_info)
   310        self.include_referenced_elements(group)
   311
   312    def finalize(self) -> None:
   313        """
   314        Make sure that all the elements we've marked as included actually appear
   315        in the ambassador_resources and envoy_resources dictionaries, so that the
   316        UI can properly connect all the dots.
   317        """
   318
   319        for key in self.element_keys.keys():
   320            amb_el_info = self.diag.ambassador_elements.get(key, None)
   321
   322            if amb_el_info:
   323                serialization = amb_el_info.get("serialization", None)
   324
   325                if serialization:
   326                    self.ambassador_resources[key] = serialization
   327
   328                # What about errors?
   329
   330            # Also make sure we have Envoy outputs for these things.
   331            envoy_el_info = self.diag.envoy_elements.get(key, None)
   332
   333            if envoy_el_info:
   334                self.envoy_resources[key] = envoy_el_info
   335
   336
   337class Diagnostics:
   338    """
   339    Information needed by the Diagnostics UI. This has to be instantiated
   340    from an IR and an EnvoyConfig (it doesn't matter which version).
   341
   342    The flow here is:
   343
   344    - create the Diagnostics object
   345    - call the .overview method to get a DiagResult that has an overview of
   346      the whole Ambassador setup, or
   347    - call the .lookup method to get a DiagResult that zeroes in on a particular
   348      chunk of the world (like a group, or a particular rkey, etc.)
   349    """
   350
   351    ir: IR
   352    econf: EnvoyConfig
   353    estats: Optional[EnvoyStats]
   354
   355    source_map: Dict[str, Dict[str, bool]]
   356
   357    reKeyIndex = re.compile(r"\.(\d+)$")
   358
   359    filter_map = {"IRAuth": "AuthService", "IRRateLimit": "RateLimitService"}
   360
   361    def __init__(self, ir: IR, econf: EnvoyConfig) -> None:
   362        self.logger = logging.getLogger("ambassador.diagnostics")
   363        self.logger.debug("---- building diagnostics")
   364
   365        self.ir = ir
   366        self.econf = econf
   367        self.estats = None
   368
   369        # A fully-qualified key is e.g. "ambassador.yaml.1" -- source location plus
   370        # object index. An unqualified key is something like "ambassador.yaml" -- no
   371        # index.
   372        #
   373        # self.source_map permits us to look up any (potentially unqualified) key
   374        # and find a list of fully-qualified keys contained in the key we looked
   375        # up.
   376        #
   377        # self.ambassador_elements has the incoming Ambassador configuration resources,
   378        # indexed by fully-qualified key.
   379        #
   380        # self.envoy_elements has the created Envoy configuration resources, indexed
   381        # by fully-qualified key.
   382
   383        self.source_map: Dict[str, Dict[str, bool]] = {}
   384        self.ambassador_elements: Dict[str, dict] = {}
   385        self.envoy_elements: Dict[str, dict] = {}
   386        self.ambassador_services: List[dict] = []
   387        self.ambassador_resolvers: List[dict] = []
   388
   389        # Warn people about upcoming deprecations.
   390
   391        warn_auth = False
   392        warn_ratelimit = False
   393
   394        for filter in self.ir.filters:
   395            if filter.kind == "IRAuth":
   396                proto = filter.get("proto") or "http"
   397
   398                if proto.lower() != "http":
   399                    warn_auth = True
   400
   401            if filter.kind == "IRRateLimit":
   402                warn_ratelimit = True
   403
   404        things_to_warn = []
   405
   406        if warn_auth:
   407            things_to_warn.append("AuthServices")
   408
   409        if warn_ratelimit:
   410            things_to_warn.append("RateLimitServices")
   411
   412        if things_to_warn:
   413            self.ir.aconf.post_notice(
   414                f'A future Ambassador version will change the GRPC protocol version for {" and ".join(things_to_warn)}. See the CHANGELOG for details.'
   415            )
   416
   417        # # Warn people about the default port change.
   418        # if self.ir.ambassador_module.service_port < 1024:
   419        #     # Does it look like they explicitly asked for this?
   420        #     amod = self.ir.aconf.get_module('ambassador')
   421        #
   422        #     if not (amod and amod.get('service_port')):
   423        #         # They did not explictly set the port. Warn them about the
   424        #         # port change.
   425        #         new_defaults = [ "port 8080 for HTTP" ]
   426        #
   427        #         if self.ir.tls_contexts:
   428        #             new_defaults.append("port 8443 for HTTPS")
   429        #
   430        #         default_ports = " and ".join(new_defaults)
   431        #
   432        #         listen_ports = [ str(l.service_port) for l in self.ir.listeners ]
   433        #         self.ir.logger.info("listen_ports %s" % listen_ports)
   434        #
   435        #         port_or_ports = "port" if (len(listen_ports) == 1) else "ports"
   436        #
   437        #         last_port = listen_ports.pop()
   438        #
   439        #         els = [ last_port ]
   440        #
   441        #         if len(listen_ports) > 0:
   442        #             els.insert(0, ", ".join(listen_ports))
   443        #
   444        #         port_nums = " and ".join(els)
   445        #
   446        #         m1 = f'Ambassador 0.60 will default to listening on {default_ports}.'
   447        #         m2 = f'You will need to change your configuration to continue using {port_or_ports} {port_nums}.'
   448        #
   449        #         self.ir.aconf.post_notice(f'{m1} {m2}')
   450
   451        # Copy in the toplevel 'error' and 'notice' sets.
   452        self.errors = self.ir.aconf.errors
   453        self.notices = self.ir.aconf.notices
   454
   455        # Next up, walk the list of Ambassador sources.
   456        for key, rsrc in self.ir.aconf.sources.items():
   457            uqkey = key  # Unqualified key, e.g. ambassador.yaml
   458            fqkey = uqkey  # Fully-qualified key, e.g. ambassador.yaml.1
   459
   460            key_index = None
   461
   462            if "rkey" in rsrc:
   463                uqkey, key_index = self.split_key(rsrc.rkey)
   464
   465            if key_index is not None:
   466                fqkey = "%s.%s" % (uqkey, key_index)
   467
   468            location, _ = self.split_key(rsrc.get("location", key))
   469
   470            self.logger.debug(
   471                "  %s (%s): UQ %s, FQ %s, LOC %s" % (key, rsrc, uqkey, fqkey, location)
   472            )
   473
   474            self.remember_source(uqkey, fqkey, location, rsrc.rkey)
   475
   476            ambassador_element: dict = self.ambassador_elements.setdefault(
   477                fqkey, {"location": location, "kind": rsrc.kind}
   478            )
   479
   480            if uqkey and (uqkey != fqkey):
   481                ambassador_element["parent"] = uqkey
   482
   483            serialization = rsrc.get("serialization", None)
   484            if serialization:
   485                if ambassador_element["kind"] == "Secret":
   486                    serialization = "kind: Secret\ndata: (elided by Ambassador)\n"
   487                ambassador_element["serialization"] = serialization
   488
   489        # Next up, the Envoy elements.
   490        for kind, elements in self.econf.elements.items():
   491            for fqkey, envoy_element in elements.items():
   492                # The key here should already be fully qualified.
   493                uqkey, _ = self.split_key(fqkey)
   494
   495                element_dict = self.envoy_elements.setdefault(fqkey, {})
   496                element_list = element_dict.setdefault(kind, [])
   497                element_list.append({k: v for k, v in envoy_element.items() if k[0] != "_"})
   498
   499        # Always generate the full group set so that we can look up groups.
   500        self.groups = {
   501            "grp-%s" % group.group_id: group
   502            for group in self.ir.groups.values()
   503            if group.location != "--diagnostics--"
   504        }
   505
   506        # Always generate the full cluster set so that we can look up clusters.
   507        self.clusters = {
   508            cluster.name: cluster
   509            for cluster in self.ir.clusters.values()
   510            if cluster.location != "--diagnostics--"
   511        }
   512
   513        # Build up our Ambassador services too (auth, ratelimit, tracing).
   514        self.ambassador_services = []
   515
   516        for filt in self.ir.filters:
   517            # self.logger.debug("FILTER %s" % filter.as_json())
   518
   519            if filt.kind in Diagnostics.filter_map:
   520                type_name = Diagnostics.filter_map[filt.kind]
   521                self.add_ambassador_service(filt, type_name)
   522
   523        if self.ir.tracing:
   524            self.add_ambassador_service(
   525                self.ir.tracing, "TracingService (%s)" % self.ir.tracing.driver
   526            )
   527
   528        self.ambassador_resolvers = []
   529        used_resolvers: Dict[str, List[str]] = {}
   530
   531        for group in self.groups.values():
   532            for mapping in group.mappings:
   533                resolver_name = mapping.resolver
   534                group_list = used_resolvers.setdefault(resolver_name, [])
   535                group_list.append(group.rkey)
   536
   537        for name, resolver in sorted(self.ir.resolvers.items()):
   538            if name in used_resolvers:
   539                self.add_ambassador_resolver(resolver, used_resolvers[name])
   540
   541    def add_ambassador_service(self, svc, type_name) -> None:
   542        """
   543        Remember information about a given Ambassador-wide service (Auth, RateLimit, Tracing).
   544
   545        :param svc: service record
   546        :param type_name: what kind of thing is this?
   547        """
   548
   549        cluster = svc.cluster
   550        urls = cluster.urls
   551
   552        svc_weight = 100.0 / len(urls)
   553
   554        for url in urls:
   555            self.ambassador_services.append(
   556                {
   557                    "type": type_name,
   558                    "_source": svc.location,
   559                    "name": url,
   560                    "cluster": cluster.name,
   561                    "_service_weight": svc_weight,
   562                }
   563            )
   564
   565    def add_ambassador_resolver(self, resolver, group_list) -> None:
   566        """
   567        Remember information about a given Ambassador-wide resolver.
   568
   569        :param resolver: resolver record
   570        :param group_list: list of groups that use this resolver
   571        """
   572
   573        self.ambassador_resolvers.append(
   574            {
   575                "kind": resolver.kind,
   576                "_source": resolver.location,
   577                "name": resolver.name,
   578                "groups": group_list,
   579            }
   580        )
   581
   582    @staticmethod
   583    def split_key(key) -> Tuple[str, Optional[str]]:
   584        """
   585        Split a key into its components (the base name and the object index).
   586
   587        :param key: possibly-qualified key
   588        :return: tuple of the base and a possible index
   589        """
   590
   591        key_base = key
   592        key_index = None
   593
   594        m = Diagnostics.reKeyIndex.search(key)
   595
   596        if m:
   597            key_base = key[: m.start()]
   598            key_index = m.group(1)
   599
   600        return key_base, key_index
   601
   602    def as_dict(self) -> dict:
   603        return {
   604            "source_map": self.source_map,
   605            "ambassador_services": self.ambassador_services,
   606            "ambassador_resolvers": self.ambassador_resolvers,
   607            "ambassador_elements": self.ambassador_elements,
   608            "envoy_elements": self.envoy_elements,
   609            "errors": self.errors,
   610            "notices": self.notices,
   611            "groups": {key: self.flattened(value) for key, value in self.groups.items()},
   612            # 'clusters': { key: value.as_dict() for key, value in self.clusters.items() },
   613            "tlscontexts": [x.as_dict() for x in self.ir.tls_contexts.values()],
   614        }
   615
   616    def flattened(self, group: IRBaseMappingGroup) -> dict:
   617        flattened = {k: v for k, v in group.as_dict().items() if k != "mappings"}
   618        flattened_mappings = []
   619
   620        for m in group["mappings"]:
   621            fm = {
   622                "_active": m["_active"],
   623                "_errored": m["_errored"],
   624                "_rkey": m["rkey"],
   625                "location": m["location"],
   626                "name": m["name"],
   627                "cluster_service": m.get("cluster", {}).get("service"),
   628                "cluster_name": m.get("cluster", {}).get("envoy_name"),
   629            }
   630
   631            if flattened["kind"] == "IRHTTPMappingGroup":
   632                fm["prefix"] = m.get("prefix")
   633
   634            rewrite = m.get("rewrite", None)
   635
   636            if rewrite:
   637                fm["rewrite"] = rewrite
   638
   639            host = m.get("host", None)
   640
   641            if host:
   642                fm["host"] = host
   643
   644            flattened_mappings.append(fm)
   645
   646        flattened["mappings"] = flattened_mappings
   647
   648        return flattened
   649
   650    def _remember_source(self, src_key: str, dest_key: str) -> None:
   651        """
   652        Link keys of active sources together. The source map lets us answer questions
   653        like 'which objects does ambassador.yaml define?' and this is the primitive
   654        that actually populates the map.
   655
   656        The src_key is where you start the lookup; the dest_key is something defined
   657        by the src_key. They can be the same.
   658
   659        :param src_key: the starting key (ambassador.yaml)
   660        :param dest_key: the destination key (ambassador.yaml.1)
   661        """
   662
   663        src_map = self.source_map.setdefault(src_key, {})
   664        src_map[dest_key] = True
   665
   666    def remember_source(
   667        self, uqkey: str, fqkey: Optional[str], location: Optional[str], dest_key: str
   668    ) -> None:
   669        """
   670        Populate the source map in various ways. A mapping from uqkey to dest_key is
   671        always added; mappings for fqkey and location are added if they are unique
   672        keys.
   673
   674        :param uqkey: unqualified source key
   675        :param fqkey: qualified source key
   676        :param location: source location
   677        :param dest_key: key of object being defined
   678        """
   679        self._remember_source(uqkey, dest_key)
   680
   681        if fqkey and (fqkey != uqkey):
   682            self._remember_source(fqkey, dest_key)
   683
   684        if location and (location != uqkey) and (location != fqkey):
   685            self._remember_source(location, dest_key)
   686
   687    def overview(self, request, estat: EnvoyStats) -> Dict[str, Any]:
   688        """
   689        Generate overview data describing the whole Ambassador setup, most
   690        notably the routing table. Returns the dictionary form of a DiagResult.
   691
   692        :param request: the Flask request being handled
   693        :param estat: current EnvoyStats
   694        :return: the dictionary form of a DiagResult
   695        """
   696
   697        result = DiagResult(self, estat, request)
   698
   699        for group in self.ir.ordered_groups():
   700            # TCPMappings are currently handled elsewhere.
   701            if isinstance(group, IRHTTPMappingGroup):
   702                result.include_httpgroup(group)
   703
   704        return result.as_dict()
   705
   706    def lookup(self, request, key: str, estat: EnvoyStats) -> Optional[Dict[str, Any]]:
   707        """
   708        Generate data describing a specific key in the Ambassador setup, and all
   709        the things connected to it. Returns the dictionary form of a DiagResult.
   710
   711        'key' can be a group key that starts with grp-, a cluster key that starts
   712        with cluster_, or a source key.
   713
   714        :param request: the Flask request being handled
   715        :param key: the key of the thing we want
   716        :param estat: current EnvoyStats
   717        :return: the dictionary form of a DiagResult
   718        """
   719
   720        result = DiagResult(self, estat, request)
   721
   722        # Typically we'll get handed a group identifier here, but we might get
   723        # other stuff too, and we have to look for all of it.
   724
   725        found: bool = False
   726
   727        if key in self.groups:
   728            # Yup, group ID.
   729            group = self.groups[key]
   730
   731            # TCPMappings are currently handled elsewhere.
   732            if isinstance(group, IRHTTPMappingGroup):
   733                result.include_httpgroup(group)
   734
   735            found = True
   736        elif key in self.clusters:
   737            result.include_cluster(self.clusters[key].as_dict())
   738            found = True
   739        elif key in self.source_map:
   740            # The source_map is set up like:
   741            #
   742            # "mapping-qotm.yaml": {
   743            #     "mapping-qotm.yaml.1": true,
   744            #     "mapping-qotm.yaml.2": true,
   745            #     "mapping-qotm.yaml.3": true
   746            # }
   747            #
   748            # so for whatever we found, we need to tell the result to
   749            # include every element in the keys of the dict stored for
   750            # our key.
   751            for subkey in self.source_map[key].keys():
   752                result.include_element(subkey)
   753                # Not a typo. Set found here, in case somehow we land on
   754                # a key with no subkeys (which should be impossible, but,
   755                # y'know).
   756                found = True
   757
   758        if found:
   759            result.finalize()
   760            return result.as_dict()
   761        else:
   762            return None

View as plain text