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.
+
+
+
+
+
+
+
+
+
+ Collection |
+ Version |
+ Resources |
+
+
+
+
+
+
+
+{{ 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.
+
+
+
+
+
+
+
+
+
+ Collection |
+ Version |
+ Resources |
+
+
+
+
+
+
+
+{{ 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
+}