istio.io/istio@v0.0.0-20240520182934-d79c90f27776/istioctl/pkg/writer/envoy/configdump/route.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 configdump 16 17 import ( 18 "encoding/json" 19 "fmt" 20 "sort" 21 "strconv" 22 "strings" 23 "text/tabwriter" 24 25 core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" 26 route "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" 27 "sigs.k8s.io/yaml" 28 29 "istio.io/istio/istioctl/pkg/util/proto" 30 pilot_util "istio.io/istio/pilot/pkg/networking/util" 31 v3 "istio.io/istio/pilot/pkg/xds/v3" 32 "istio.io/istio/pkg/util/sets" 33 ) 34 35 // RouteFilter is used to pass filter information into route based config writer print functions 36 type RouteFilter struct { 37 Name string 38 Verbose bool 39 } 40 41 // Verify returns true if the passed route matches the filter fields 42 func (r *RouteFilter) Verify(route *route.RouteConfiguration) bool { 43 if r.Name != "" && r.Name != route.Name { 44 return false 45 } 46 return true 47 } 48 49 // PrintRouteSummary prints a summary of the relevant routes in the config dump to the ConfigWriter stdout 50 func (c *ConfigWriter) PrintRouteSummary(filter RouteFilter) error { 51 w, routes, err := c.setupRouteConfigWriter() 52 if err != nil { 53 return err 54 } 55 if filter.Verbose { 56 fmt.Fprintln(w, "NAME\tVHOST NAME\tDOMAINS\tMATCH\tVIRTUAL SERVICE") 57 } else { 58 fmt.Fprintln(w, "NAME\tVIRTUAL HOSTS") 59 } 60 for _, route := range routes { 61 if filter.Verify(route) { 62 if includeConfigType { 63 route.Name = fmt.Sprintf("route/%s", route.Name) 64 } 65 if filter.Verbose { 66 for _, vhosts := range route.GetVirtualHosts() { 67 for _, r := range vhosts.Routes { 68 if !isPassthrough(r.GetAction()) { 69 fmt.Fprintf(w, "%v\t%s\t%s\t%s\t%s\n", 70 route.Name, 71 vhosts.Name, 72 describeRouteDomains(vhosts.GetDomains()), 73 describeMatch(r.GetMatch()), 74 describeManagement(r.GetMetadata())) 75 } 76 } 77 if len(vhosts.Routes) == 0 { 78 fmt.Fprintf(w, "%v\t%s\t%s\t%s\t%s\n", 79 route.Name, 80 vhosts.Name, 81 describeRouteDomains(vhosts.GetDomains()), 82 "/*", 83 "404") 84 } 85 } 86 } else { 87 fmt.Fprintf(w, "%v\t%v\n", route.Name, len(route.GetVirtualHosts())) 88 } 89 } 90 } 91 return w.Flush() 92 } 93 94 func describeRouteDomains(domains []string) string { 95 if len(domains) == 0 { 96 return "" 97 } 98 if len(domains) == 1 { 99 return domains[0] 100 } 101 102 // Return the shortest non-numeric domain. Count of domains seems uninteresting. 103 max := 2 104 withoutPort := make([]string, 0, len(domains)) 105 for _, d := range domains { 106 if !strings.Contains(d, ":") { 107 withoutPort = append(withoutPort, d) 108 // if the domain contains IPv6, such as [fd00:10:96::7fc7] and [fd00:10:96::7fc7]:8090 109 } else if strings.Count(d, ":") > 2 { 110 // if the domain is only a IPv6 address, such as [fd00:10:96::7fc7], append it 111 if strings.HasSuffix(d, "]") { 112 withoutPort = append(withoutPort, d) 113 } 114 } 115 } 116 withoutPort = unexpandDomains(withoutPort) 117 if len(withoutPort) > max { 118 ret := strings.Join(withoutPort[:max], ", ") 119 return fmt.Sprintf("%s + %d more...", ret, len(withoutPort)-max) 120 } 121 return strings.Join(withoutPort, ", ") 122 } 123 124 func unexpandDomains(domains []string) []string { 125 unique := sets.New(domains...) 126 shouldDelete := sets.New[string]() 127 for _, h := range domains { 128 stripFull := strings.TrimSuffix(h, ".svc.cluster.local") 129 if unique.Contains(stripFull) && stripFull != h { 130 shouldDelete.Insert(h) 131 } 132 stripPartial := strings.TrimSuffix(h, ".svc") 133 if unique.Contains(stripPartial) && stripPartial != h { 134 shouldDelete.Insert(h) 135 } 136 } 137 // Filter from original list to keep original order 138 ret := make([]string, 0, len(domains)) 139 for _, h := range domains { 140 if !shouldDelete.Contains(h) { 141 ret = append(ret, h) 142 } 143 } 144 return ret 145 } 146 147 func describeManagement(metadata *core.Metadata) string { 148 if metadata == nil { 149 return "" 150 } 151 istioMetadata, ok := metadata.FilterMetadata[pilot_util.IstioMetadataKey] 152 if !ok { 153 return "" 154 } 155 config, ok := istioMetadata.Fields["config"] 156 if !ok { 157 return "" 158 } 159 return renderConfig(config.GetStringValue()) 160 } 161 162 func renderConfig(configPath string) string { 163 if strings.HasPrefix(configPath, "/apis/networking.istio.io/v1alpha3/namespaces/") { 164 pieces := strings.Split(configPath, "/") 165 if len(pieces) != 8 { 166 return "" 167 } 168 return fmt.Sprintf("%s.%s", pieces[7], pieces[5]) 169 } 170 return "<unknown>" 171 } 172 173 // PrintRouteDump prints the relevant routes in the config dump to the ConfigWriter stdout 174 func (c *ConfigWriter) PrintRouteDump(filter RouteFilter, outputFormat string) error { 175 _, routes, err := c.setupRouteConfigWriter() 176 if err != nil { 177 return err 178 } 179 filteredRoutes := make(proto.MessageSlice, 0, len(routes)) 180 for _, route := range routes { 181 if filter.Verify(route) { 182 filteredRoutes = append(filteredRoutes, route) 183 } 184 } 185 out, err := json.MarshalIndent(filteredRoutes, "", " ") 186 if err != nil { 187 return err 188 } 189 if outputFormat == "yaml" { 190 if out, err = yaml.JSONToYAML(out); err != nil { 191 return err 192 } 193 } 194 fmt.Fprintln(c.Stdout, string(out)) 195 return nil 196 } 197 198 func (c *ConfigWriter) setupRouteConfigWriter() (*tabwriter.Writer, []*route.RouteConfiguration, error) { 199 routes, err := c.retrieveSortedRouteSlice() 200 if err != nil { 201 return nil, nil, err 202 } 203 w := new(tabwriter.Writer).Init(c.Stdout, 0, 8, 5, ' ', 0) 204 return w, routes, nil 205 } 206 207 func (c *ConfigWriter) retrieveSortedRouteSlice() ([]*route.RouteConfiguration, error) { 208 if c.configDump == nil { 209 return nil, fmt.Errorf("config writer has not been primed") 210 } 211 routeDump, err := c.configDump.GetRouteConfigDump() 212 if err != nil { 213 return nil, err 214 } 215 routes := make([]*route.RouteConfiguration, 0) 216 for _, r := range routeDump.DynamicRouteConfigs { 217 if r.RouteConfig != nil { 218 routeTyped := &route.RouteConfiguration{} 219 // Support v2 or v3 in config dump. See ads.go:RequestedTypes for more info. 220 r.RouteConfig.TypeUrl = v3.RouteType 221 err = r.RouteConfig.UnmarshalTo(routeTyped) 222 if err != nil { 223 return nil, err 224 } 225 routes = append(routes, routeTyped) 226 } 227 } 228 for _, r := range routeDump.StaticRouteConfigs { 229 if r.RouteConfig != nil { 230 routeTyped := &route.RouteConfiguration{} 231 // Support v2 or v3 in config dump. See ads.go:RequestedTypes for more info. 232 r.RouteConfig.TypeUrl = v3.RouteType 233 err = r.RouteConfig.UnmarshalTo(routeTyped) 234 if err != nil { 235 return nil, err 236 } 237 routes = append(routes, routeTyped) 238 } 239 } 240 if len(routes) == 0 { 241 return nil, fmt.Errorf("no routes found") 242 } 243 sort.Slice(routes, func(i, j int) bool { 244 iName, err := strconv.Atoi(routes[i].Name) 245 if err != nil { 246 return false 247 } 248 jName, err := strconv.Atoi(routes[j].Name) 249 if err != nil { 250 return false 251 } 252 return iName < jName 253 }) 254 return routes, nil 255 } 256 257 func isPassthrough(action any) bool { 258 a, ok := action.(*route.Route_Route) 259 if !ok { 260 return false 261 } 262 cl, ok := a.Route.ClusterSpecifier.(*route.RouteAction_Cluster) 263 if !ok { 264 return false 265 } 266 return cl.Cluster == pilot_util.PassthroughCluster 267 }