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  }