istio.io/istio@v0.0.0-20240520182934-d79c90f27776/pkg/ctrlz/topics/collection.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 topics 16 17 import ( 18 "fmt" 19 "html/template" 20 "net/http" 21 "sort" 22 "strings" 23 24 "sigs.k8s.io/yaml" 25 26 "istio.io/istio/pkg/ctrlz/fw" 27 "istio.io/istio/pkg/ctrlz/topics/assets" 28 ) 29 30 // ReadableCollection is a staticCollection collection of entries to be exposed via CtrlZ. 31 type ReadableCollection interface { 32 Name() string 33 Keys() (keys []string, err error) 34 Get(id string) (any, error) 35 } 36 37 // collection topic is a Topic fw.implementation that exposes a set of collections through CtrlZ. 38 type collectionTopic struct { 39 title string 40 prefix string 41 collections []ReadableCollection 42 43 mainTmpl *template.Template 44 listTmpl *template.Template 45 itemTmpl *template.Template 46 } 47 48 var _ fw.Topic = &collectionTopic{} 49 50 // Title is implementation of Topic.Title. 51 func (c *collectionTopic) Title() string { 52 return c.title 53 } 54 55 // Prefix is implementation of Topic.Prefix. 56 func (c *collectionTopic) Prefix() string { 57 return c.prefix 58 } 59 60 // Activate is implementation of Topic.Activate. 61 func (c *collectionTopic) Activate(context fw.TopicContext) { 62 l := template.Must(context.Layout().Clone()) 63 c.mainTmpl = assets.ParseTemplate(l, "templates/collection/main.html") 64 65 l = template.Must(context.Layout().Clone()) 66 c.listTmpl = assets.ParseTemplate(l, "templates/collection/list.html") 67 68 l = template.Must(context.Layout().Clone()) 69 c.itemTmpl = assets.ParseTemplate(l, "templates/collection/item.html") 70 71 _ = context.HTMLRouter(). 72 StrictSlash(true). 73 NewRoute(). 74 PathPrefix("/"). 75 Methods("GET"). 76 HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 77 parts := strings.SplitN(req.URL.Path, "/", 4) 78 parts = parts[2:] // Skip the empty and title parts. 79 80 switch len(parts) { 81 case 1: 82 if parts[0] == "" { 83 c.handleMain(w, req) 84 } else { 85 c.handleCollection(w, req, parts[0]) 86 } 87 88 case 2: 89 c.handleItem(w, req, parts[0], parts[1]) 90 91 default: 92 c.handleError(w, req, fmt.Sprintf("InvalidUrl %s", req.URL.Path)) 93 } 94 }) 95 } 96 97 // mainContext is passed to the template processor and carries information that is used by the main template. 98 type mainContext struct { 99 Title string 100 Collections []string 101 Error string 102 } 103 104 func (c *collectionTopic) handleMain(w http.ResponseWriter, _ *http.Request) { 105 context := mainContext{} 106 names := make([]string, 0, len(c.collections)) 107 for _, n := range c.collections { 108 names = append(names, n.Name()) 109 } 110 sort.Strings(names) 111 context.Collections = names 112 context.Title = c.title 113 fw.RenderHTML(w, c.mainTmpl, context) 114 } 115 116 // listContext is passed to the template processor and carries information that is used by the list template. 117 type listContext struct { 118 Collection string 119 Keys []string 120 Error string 121 } 122 123 func (c *collectionTopic) handleCollection(w http.ResponseWriter, _ *http.Request, collection string) { 124 k, err := c.listCollection(collection) 125 context := listContext{} 126 if err == nil { 127 context.Collection = collection 128 context.Keys = k 129 } else { 130 context.Error = err.Error() 131 } 132 fw.RenderHTML(w, c.listTmpl, context) 133 } 134 135 // itemContext is passed to the template processor and carries information that is used by the list template. 136 type itemContext struct { 137 Collection string 138 Key string 139 Value any 140 Error string 141 } 142 143 func (c *collectionTopic) handleItem(w http.ResponseWriter, _ *http.Request, collection, key string) { 144 v, err := c.getItem(collection, key) 145 context := itemContext{} 146 if err == nil { 147 switch v.(type) { 148 case string: 149 150 default: 151 var b []byte 152 if b, err = yaml.Marshal(v); err != nil { 153 context.Error = err.Error() 154 break 155 } 156 v = string(b) 157 } 158 159 context.Collection = collection 160 context.Key = key 161 context.Value = v 162 } else { 163 context.Error = err.Error() 164 } 165 fw.RenderHTML(w, c.itemTmpl, context) 166 } 167 168 func (c *collectionTopic) handleError(w http.ResponseWriter, _ *http.Request, errorText string) { 169 fw.RenderHTML(w, c.mainTmpl, mainContext{Error: errorText}) 170 } 171 172 func (c *collectionTopic) listCollection(name string) ([]string, error) { 173 for _, col := range c.collections { 174 if col.Name() == name { 175 return col.Keys() 176 } 177 } 178 179 return nil, fmt.Errorf("collection not found: %q", name) 180 } 181 182 func (c *collectionTopic) getItem(collection string, id string) (any, error) { 183 for _, col := range c.collections { 184 if col.Name() == collection { 185 return col.Get(id) 186 } 187 } 188 189 return nil, fmt.Errorf("collection not found: %q", collection) 190 } 191 192 // NewCollectionTopic creates a new custom topic that exposes the provided collections. 193 func NewCollectionTopic(title string, prefix string, collections ...ReadableCollection) fw.Topic { 194 return &collectionTopic{ 195 title: title, 196 prefix: prefix, 197 collections: collections, 198 } 199 } 200 201 // NewStaticCollection creates a static collection from the given set of items. 202 func NewStaticCollection(name string, items map[string]any) ReadableCollection { 203 return &staticCollection{ 204 name: name, 205 items: items, 206 } 207 } 208 209 // staticCollection is a ReadableCollection implementation that operates on static data that is supplied 210 // during construction. 211 type staticCollection struct { 212 name string 213 items map[string]any 214 } 215 216 var _ ReadableCollection = &staticCollection{} 217 218 // Name is implementation of ReadableCollection.Name. 219 func (r *staticCollection) Name() string { 220 return r.name 221 } 222 223 // Keys is implementation of ReadableCollection.Keys. 224 func (r *staticCollection) Keys() ([]string, error) { 225 keys := make([]string, 0, len(r.items)) 226 for k := range r.items { 227 keys = append(keys, k) 228 } 229 sort.Strings(keys) 230 231 return keys, nil 232 } 233 234 // Get is implementation of ReadableCollection.Get. 235 func (r *staticCollection) Get(id string) (any, error) { 236 return r.items[id], nil 237 }