1 /* 2 * 3 * Copyright 2021 gRPC authors. 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); 6 * you may not use this file except in compliance with the License. 7 * You may obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 */ 17 18 package xdsresource 19 20 import ( 21 "net/url" 22 "sort" 23 "strings" 24 ) 25 26 // FederationScheme is the scheme of a federation resource name. 27 const FederationScheme = "xdstp" 28 29 // Name contains the parsed component of an xDS resource name. 30 // 31 // An xDS resource name is in the format of 32 // xdstp://[{authority}]/{resource type}/{id/*}?{context parameters}{#processing directive,*} 33 // 34 // See 35 // https://github.com/cncf/xds/blob/main/proposals/TP1-xds-transport-next.md#uri-based-xds-resource-names 36 // for details, and examples. 37 type Name struct { 38 Scheme string 39 Authority string 40 Type string 41 ID string 42 43 ContextParams map[string]string 44 45 processingDirective string 46 } 47 48 // ParseName splits the name and returns a struct representation of the Name. 49 // 50 // If the name isn't a valid new-style xDS name, field ID is set to the input. 51 // Note that this is not an error, because we still support the old-style 52 // resource names (those not starting with "xdstp:"). 53 // 54 // The caller can tell if the parsing is successful by checking the returned 55 // Scheme. 56 func ParseName(name string) *Name { 57 if !strings.Contains(name, "://") { 58 // Only the long form URL, with ://, is valid. 59 return &Name{ID: name} 60 } 61 parsed, err := url.Parse(name) 62 if err != nil { 63 return &Name{ID: name} 64 } 65 66 ret := &Name{ 67 Scheme: parsed.Scheme, 68 Authority: parsed.Host, 69 } 70 split := strings.SplitN(parsed.Path, "/", 3) 71 if len(split) < 3 { 72 // Path is in the format of "/type/id". There must be at least 3 73 // segments after splitting. 74 return &Name{ID: name} 75 } 76 ret.Type = split[1] 77 ret.ID = split[2] 78 if len(parsed.Query()) != 0 { 79 ret.ContextParams = make(map[string]string) 80 for k, vs := range parsed.Query() { 81 if len(vs) > 0 { 82 // We only keep one value of each key. Behavior for multiple values 83 // is undefined. 84 ret.ContextParams[k] = vs[0] 85 } 86 } 87 } 88 // TODO: processing directive (the part comes after "#" in the URL, stored 89 // in parsed.RawFragment) is kept but not processed. Add support for that 90 // when it's needed. 91 ret.processingDirective = parsed.RawFragment 92 return ret 93 } 94 95 // String returns a canonicalized string of name. The context parameters are 96 // sorted by the keys. 97 func (n *Name) String() string { 98 if n.Scheme == "" { 99 return n.ID 100 } 101 102 // Sort and build query. 103 keys := make([]string, 0, len(n.ContextParams)) 104 for k := range n.ContextParams { 105 keys = append(keys, k) 106 } 107 sort.Strings(keys) 108 var pairs []string 109 for _, k := range keys { 110 pairs = append(pairs, strings.Join([]string{k, n.ContextParams[k]}, "=")) 111 } 112 rawQuery := strings.Join(pairs, "&") 113 114 path := n.Type 115 if n.ID != "" { 116 path = "/" + path + "/" + n.ID 117 } 118 119 tempURL := &url.URL{ 120 Scheme: n.Scheme, 121 Host: n.Authority, 122 Path: path, 123 RawQuery: rawQuery, 124 RawFragment: n.processingDirective, 125 } 126 return tempURL.String() 127 } 128