diff --git a/galley/pkg/server/server.go b/galley/pkg/server/server.go index 96889cc530a4..76dc9376bdee 100644 --- a/galley/pkg/server/server.go +++ b/galley/pkg/server/server.go @@ -38,7 +38,9 @@ import ( "istio.io/istio/galley/pkg/source/kube/schema" "istio.io/istio/galley/pkg/source/kube/schema/check" "istio.io/istio/pkg/ctrlz" + "istio.io/istio/pkg/ctrlz/fw" "istio.io/istio/pkg/log" + configz "istio.io/istio/pkg/mcp/configz/server" "istio.io/istio/pkg/mcp/creds" "istio.io/istio/pkg/mcp/monitoring" mcprate "istio.io/istio/pkg/mcp/rate" @@ -219,7 +221,7 @@ func newServer(a *Args, p patchTable) (*Server, error) { mcp.RegisterAggregatedMeshConfigServiceServer(s.grpcServer, s.mcp) mcp.RegisterResourceSourceServer(s.grpcServer, s.mcpSource) - s.controlZ, _ = ctrlz.Run(a.IntrospectionOptions, nil) + s.controlZ, _ = ctrlz.Run(a.IntrospectionOptions, []fw.Topic{configz.CreateTopic(distributor)}) return s, nil } diff --git a/pkg/mcp/configz/server/assets.gen.go b/pkg/mcp/configz/server/assets.gen.go new file mode 100644 index 000000000000..d5e815cfa4ac --- /dev/null +++ b/pkg/mcp/configz/server/assets.gen.go @@ -0,0 +1,340 @@ +// Code generated by go-bindata. +// sources: +// assets/templates/config.html +// DO NOT EDIT! + +package configz + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + "time" +) +type asset struct { + bytes []byte + info os.FileInfo +} + +type bindataFileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time +} + +func (fi bindataFileInfo) Name() string { + return fi.name +} +func (fi bindataFileInfo) Size() int64 { + return fi.size +} +func (fi bindataFileInfo) Mode() os.FileMode { + return fi.mode +} +func (fi bindataFileInfo) ModTime() time.Time { + return fi.modTime +} +func (fi bindataFileInfo) IsDir() bool { + return false +} +func (fi bindataFileInfo) Sys() interface{} { + return nil +} + +var _assetsTemplatesConfigHtml = []byte(`{{ define "content" }} + +

+ The Mesh Configuration Protocol (MCP) server state for this process. MCP can serve multiple different types of snapshots to different clients. +

+ + + + +
+ + + + + + + + + + + +
CollectionVersionResources
+
+{{ template "last-refresh" .}} + + + +{{ end }} +`) + +func assetsTemplatesConfigHtmlBytes() ([]byte, error) { + return _assetsTemplatesConfigHtml, nil +} + +func assetsTemplatesConfigHtml() (*asset, error) { + bytes, err := assetsTemplatesConfigHtmlBytes() + if err != nil { + return nil, err + } + + info := bindataFileInfo{name: "assets/templates/config.html", size: 0, mode: os.FileMode(0), modTime: time.Unix(0, 0)} + a := &asset{bytes: bytes, info: info} + return a, nil +} + +// Asset loads and returns the asset for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func Asset(name string) ([]byte, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("Asset %s can't read by error: %v", name, err) + } + return a.bytes, nil + } + return nil, fmt.Errorf("Asset %s not found", name) +} + +// MustAsset is like Asset but panics when Asset would return an error. +// It simplifies safe initialization of global variables. +func MustAsset(name string) []byte { + a, err := Asset(name) + if err != nil { + panic("asset: Asset(" + name + "): " + err.Error()) + } + + return a +} + +// AssetInfo loads and returns the asset info for the given name. +// It returns an error if the asset could not be found or +// could not be loaded. +func AssetInfo(name string) (os.FileInfo, error) { + cannonicalName := strings.Replace(name, "\\", "/", -1) + if f, ok := _bindata[cannonicalName]; ok { + a, err := f() + if err != nil { + return nil, fmt.Errorf("AssetInfo %s can't read by error: %v", name, err) + } + return a.info, nil + } + return nil, fmt.Errorf("AssetInfo %s not found", name) +} + +// AssetNames returns the names of the assets. +func AssetNames() []string { + names := make([]string, 0, len(_bindata)) + for name := range _bindata { + names = append(names, name) + } + return names +} + +// _bindata is a table, holding each asset generator, mapped to its name. +var _bindata = map[string]func() (*asset, error){ + "assets/templates/config.html": assetsTemplatesConfigHtml, +} + +// AssetDir returns the file names below a certain +// directory embedded in the file by go-bindata. +// For example if you run go-bindata on data/... and data contains the +// following hierarchy: +// data/ +// foo.txt +// img/ +// a.png +// b.png +// then AssetDir("data") would return []string{"foo.txt", "img"} +// AssetDir("data/img") would return []string{"a.png", "b.png"} +// AssetDir("foo.txt") and AssetDir("notexist") would return an error +// AssetDir("") will return []string{"data"}. +func AssetDir(name string) ([]string, error) { + node := _bintree + if len(name) != 0 { + cannonicalName := strings.Replace(name, "\\", "/", -1) + pathList := strings.Split(cannonicalName, "/") + for _, p := range pathList { + node = node.Children[p] + if node == nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + } + } + if node.Func != nil { + return nil, fmt.Errorf("Asset %s not found", name) + } + rv := make([]string, 0, len(node.Children)) + for childName := range node.Children { + rv = append(rv, childName) + } + return rv, nil +} + +type bintree struct { + Func func() (*asset, error) + Children map[string]*bintree +} +var _bintree = &bintree{nil, map[string]*bintree{ + "assets": &bintree{nil, map[string]*bintree{ + "templates": &bintree{nil, map[string]*bintree{ + "config.html": &bintree{assetsTemplatesConfigHtml, map[string]*bintree{}}, + }}, + }}, +}} + +// RestoreAsset restores an asset under the given directory +func RestoreAsset(dir, name string) error { + data, err := Asset(name) + if err != nil { + return err + } + info, err := AssetInfo(name) + if err != nil { + return err + } + err = os.MkdirAll(_filePath(dir, filepath.Dir(name)), os.FileMode(0755)) + if err != nil { + return err + } + err = ioutil.WriteFile(_filePath(dir, name), data, info.Mode()) + if err != nil { + return err + } + err = os.Chtimes(_filePath(dir, name), info.ModTime(), info.ModTime()) + if err != nil { + return err + } + return nil +} + +// RestoreAssets restores an asset under the given directory recursively +func RestoreAssets(dir, name string) error { + children, err := AssetDir(name) + // File + if err != nil { + return RestoreAsset(dir, name) + } + // Dir + for _, child := range children { + err = RestoreAssets(dir, filepath.Join(name, child)) + if err != nil { + return err + } + } + return nil +} + +func _filePath(dir, name string) string { + cannonicalName := strings.Replace(name, "\\", "/", -1) + return filepath.Join(append([]string{dir}, strings.Split(cannonicalName, "/")...)...) +} + diff --git a/pkg/mcp/configz/server/assets.go b/pkg/mcp/configz/server/assets.go new file mode 100644 index 000000000000..dbc0faec3503 --- /dev/null +++ b/pkg/mcp/configz/server/assets.go @@ -0,0 +1,17 @@ +// Copyright 2018 Istio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:generate $GOPATH/src/istio.io/istio/bin/go-bindata.sh --nocompress --nometadata --pkg configz -o assets.gen.go assets/... + +package configz diff --git a/pkg/mcp/configz/server/assets/templates/config.html b/pkg/mcp/configz/server/assets/templates/config.html new file mode 100644 index 000000000000..93f2cc972856 --- /dev/null +++ b/pkg/mcp/configz/server/assets/templates/config.html @@ -0,0 +1,128 @@ +{{ define "content" }} + +

+ The Mesh Configuration Protocol (MCP) server state for this process. MCP can serve multiple different types of snapshots to different clients. +

+ + + + +
+ + + + + + + + + + + +
CollectionVersionResources
+
+{{ template "last-refresh" .}} + + + +{{ end }} diff --git a/pkg/mcp/configz/server/configz.go b/pkg/mcp/configz/server/configz.go new file mode 100644 index 000000000000..30ab8d10aa52 --- /dev/null +++ b/pkg/mcp/configz/server/configz.go @@ -0,0 +1,94 @@ +// Copyright 2019 Istio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package configz + +import ( + "html/template" + "net/http" + + "istio.io/istio/pkg/ctrlz" + "istio.io/istio/pkg/ctrlz/fw" + "istio.io/istio/pkg/mcp/snapshot" +) + +// configzTopic topic is a Topic fw.implementation that exposes the state info for different snapshots galley is serving. +type configzTopic struct { + tmpl *template.Template + + topic SnapshotTopic +} + +var _ fw.Topic = &configzTopic{} + +// SnapshotTopic defines the expected interface for producing configz data from MCP snapshots. +type SnapshotTopic interface { + GetSnapshotInfo(group string) []snapshot.Info + GetGroups() []string +} + +// Register the Configz topic for the snapshots. +func Register(topic SnapshotTopic) { + ctrlz.RegisterTopic(CreateTopic(topic)) +} + +// CreateTopic creates and returns a configz topic for the snapshots. It does not do any registration. +func CreateTopic(topic SnapshotTopic) fw.Topic { + return &configzTopic{ + topic: topic, + } +} + +// Title is implementation of Topic.Title. +func (c *configzTopic) Title() string { + return "Config" +} + +// Prefix is implementation of Topic.Prefix. +func (c *configzTopic) Prefix() string { + return "config" +} + +type data struct { + Snapshots []snapshot.Info + Groups []string +} + +// Activate is implementation of Topic.Activate. +func (c *configzTopic) Activate(context fw.TopicContext) { + l := template.Must(context.Layout().Clone()) + c.tmpl = template.Must(l.Parse(string(MustAsset("assets/templates/config.html")))) + + _ = context.HTMLRouter().StrictSlash(true).NewRoute().Path("/").HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + d := c.collectData("") + fw.RenderHTML(w, c.tmpl, d) + }) + + _ = context.JSONRouter().StrictSlash(true).NewRoute().Methods("GET").Path("/").HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + keys, ok := req.URL.Query()["group"] + group := "" + if ok && len(keys[0]) > 1 { + group = keys[0] + } + d := c.collectData(group) + fw.RenderJSON(w, http.StatusOK, d) + }) +} + +func (c *configzTopic) collectData(group string) *data { + return &data{ + Snapshots: c.topic.GetSnapshotInfo(group), + Groups: c.topic.GetGroups(), + } +} diff --git a/pkg/mcp/configz/server/configz_test.go b/pkg/mcp/configz/server/configz_test.go new file mode 100644 index 000000000000..f57ba6f25aeb --- /dev/null +++ b/pkg/mcp/configz/server/configz_test.go @@ -0,0 +1,120 @@ +// Copyright 2018 Istio Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package configz + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "testing" + "time" + + "istio.io/istio/pkg/mcp/source" + "istio.io/istio/pkg/mcp/testing/groups" + + "github.com/gogo/protobuf/types" + + "istio.io/istio/pkg/ctrlz" + "istio.io/istio/pkg/ctrlz/fw" + "istio.io/istio/pkg/mcp/snapshot" + mcptest "istio.io/istio/pkg/mcp/testing" +) + +const testK8sCollection = "k8s/core/v1/nodes" + +func TestConfigZ(t *testing.T) { + s, err := mcptest.NewServer(0, []source.CollectionOptions{{Name: testK8sCollection}}) + if err != nil { + t.Fatal(err) + } + defer func() { _ = s.Close() }() + + b := snapshot.NewInMemoryBuilder() + b.SetVersion(testK8sCollection, "23") + err = b.SetEntry(testK8sCollection, "foo", "v0", time.Time{}, nil, nil, &types.Empty{}) + if err != nil { + t.Fatalf("Setting an entry should not have failed: %v", err) + } + + s.Cache.SetSnapshot(groups.Default, b.Build()) + + o := ctrlz.DefaultOptions() + cz, _ := ctrlz.Run(o, []fw.Topic{CreateTopic(s.Cache)}) + defer cz.Close() + + baseURL := fmt.Sprintf("http://%s:%d", o.Address, o.Port) + + t.Run("configj with 1 request", func(tt *testing.T) { testConfigJWithOneRequest(tt, baseURL) }) +} + +func testConfigJWithOneRequest(t *testing.T, baseURL string) { + t.Helper() + + data := request(t, baseURL+"/configj/") + + m := make(map[string]interface{}) + err := json.Unmarshal([]byte(data), &m) + if err != nil { + t.Fatalf("Should have unmarshalled json: %v", err) + } + + exists := false + for _, group := range m["Groups"].([]interface{}) { + if group.(string) == groups.Default { + exists = true + break + } + } + if !exists { + t.Fatalf("Should have contained metadata: %v", data) + } + + exists = false + for _, collection := range m["Snapshots"].([]interface{}) { + if collection.(map[string]interface{})["Collection"].(string) == testK8sCollection { + exists = true + break + } + } + if !exists { + t.Fatalf("Should have contained supported collections: %v", data) + } + +} + +func request(t *testing.T, url string) string { + var e error + for i := 1; i < 10; i++ { + resp, err := http.Get(url) + if err != nil { + e = err + time.Sleep(time.Millisecond * 100) + continue + } + defer func() { _ = resp.Body.Close() }() + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + e = err + time.Sleep(time.Millisecond * 100) + continue + } + + return string(body) + } + + t.Fatalf("Unable to complete get request: url='%s', last err='%v'", url, e) + return "" +} diff --git a/pkg/mcp/snapshot/snapshot.go b/pkg/mcp/snapshot/snapshot.go index 482e684bb764..fe7e436f8e06 100644 --- a/pkg/mcp/snapshot/snapshot.go +++ b/pkg/mcp/snapshot/snapshot.go @@ -15,10 +15,12 @@ package snapshot import ( + "sort" "sync" "time" mcp "istio.io/api/mcp/v1alpha1" + "istio.io/istio/galley/pkg/metadata" "istio.io/istio/pkg/log" "istio.io/istio/pkg/mcp/source" ) @@ -31,6 +33,16 @@ type Snapshot interface { Version(collection string) string } +// Info is used for configz +type Info struct { + // Collection of mcp resource + Collection string + // Version of the resource + Version string + // Names of the resource entries. + Names []string +} + // Cache is a snapshot-based cache that maintains a single versioned // snapshot of responses per group of clients. Cache consistently replies with the // latest snapshot. @@ -227,3 +239,57 @@ func (c *Cache) Status(group string) *StatusInfo { } return nil } + +// GetGroups returns all groups of snapshots that the server layer is serving. +func (c *Cache) GetGroups() []string { + c.mu.Lock() + defer c.mu.Unlock() + + groups := make([]string, 0, len(c.snapshots)) + + for t := range c.snapshots { + groups = append(groups, t) + } + sort.Strings(groups) + + return groups +} + +// GetSnapshotInfo return the snapshots information +func (c *Cache) GetSnapshotInfo(group string) []Info { + + //if the group is empty, then use the default one + if group == "" { + group = c.GetGroups()[0] + } + + if snapshot, ok := c.snapshots[group]; ok { + + snapshots := make([]Info, 0, len(metadata.Types.All())) + collections := make([]string, 0, len(metadata.Types.All())) + + for _, info := range metadata.Types.All() { + collections = append(collections, info.Collection.String()) + } + //sort the collections + sort.Strings(collections) + + for _, collection := range collections { + entrieNames := make([]string, 0, len(snapshot.Resources(collection))) + for _, entry := range snapshot.Resources(collection) { + entrieNames = append(entrieNames, entry.Metadata.Name) + } + //sort the mcp resource names + sort.Strings(entrieNames) + + info := Info{ + Collection: collection, + Version: snapshot.Version(collection), + Names: entrieNames, + } + snapshots = append(snapshots, info) + } + return snapshots + } + return nil +}